krz/brand-bench
A dynamic brand documentation generator with Ollama integration.
clone: git clone https://gitbay.org/krz/brand-bench.git
main: src/components/SettingsPanel.tsx · raw
1import { useState, useEffect } from 'react';
2import type { OllamaSettings } from '../hooks/useSettings';
3import { testOllamaConnection } from '../engine/aiGenerator';
4
5interface Props {
6 settings: OllamaSettings;
7 onChange: (next: Partial<OllamaSettings>) => void;
8 onClose: () => void;
9}
10
11type TestState =
12 | { status: 'idle' }
13 | { status: 'testing' }
14 | { status: 'ok'; models: string[] }
15 | { status: 'error'; message: string };
16
17export function SettingsPanel({ settings, onChange, onClose }: Props) {
18 const [url, setUrl] = useState(settings.baseUrl);
19 const [model, setModel] = useState(settings.model);
20 const [test, setTest] = useState<TestState>({ status: 'idle' });
21
22 // Commit field on blur/enter
23 const commitUrl = () => onChange({ baseUrl: url });
24 const commitModel = (m: string) => { setModel(m); onChange({ model: m }); };
25
26 const runTest = async () => {
27 onChange({ baseUrl: url }); // save current URL first
28 setTest({ status: 'testing' });
29 try {
30 const models = await testOllamaConnection(url);
31 setTest({ status: 'ok', models });
32 } catch (err) {
33 setTest({ status: 'error', message: (err as Error).message });
34 }
35 };
36
37 // Close on Escape
38 useEffect(() => {
39 const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
40 window.addEventListener('keydown', handler);
41 return () => window.removeEventListener('keydown', handler);
42 }, [onClose]);
43
44 return (
45 <div className="settings-overlay" onClick={onClose}>
46 <div className="settings-panel" onClick={e => e.stopPropagation()}>
47 <div className="settings-header">
48 <span className="settings-title">Settings</span>
49 <button className="settings-close" onClick={onClose}>×</button>
50 </div>
51
52 <div className="settings-body">
53 {/* AI toggle */}
54 <div className="settings-section">
55 <div className="settings-section-label">AI Generation</div>
56 <label className="settings-toggle-row">
57 <div className="settings-toggle-info">
58 <span className="settings-toggle-name">Use Ollama</span>
59 <span className="settings-toggle-desc">
60 Replace template generation with a local AI model
61 </span>
62 </div>
63 <button
64 className={`toggle${settings.enabled ? ' on' : ''}`}
65 onClick={() => onChange({ enabled: !settings.enabled })}
66 role="switch"
67 aria-checked={settings.enabled}
68 >
69 <span className="toggle-thumb" />
70 </button>
71 </label>
72 </div>
73
74 {/* Ollama config */}
75 {settings.enabled && (
76 <div className="settings-section">
77 <div className="settings-section-label">Ollama</div>
78
79 <div className="settings-field">
80 <label className="settings-field-label">Server URL</label>
81 <div className="settings-field-row">
82 <input
83 className="settings-input"
84 type="url"
85 value={url}
86 onChange={e => setUrl(e.target.value)}
87 onBlur={commitUrl}
88 onKeyDown={e => e.key === 'Enter' && commitUrl()}
89 placeholder="empty = same-origin proxy (recommended)"
90 spellCheck={false}
91 />
92 <button
93 className={`btn settings-test-btn${test.status === 'testing' ? ' testing' : ''}`}
94 onClick={runTest}
95 disabled={test.status === 'testing'}
96 >
97 {test.status === 'testing' ? 'Testing…' : 'Test'}
98 </button>
99 </div>
100
101 {test.status === 'ok' && (
102 <div className="settings-status ok">
103 Connected · {test.models.length} model{test.models.length !== 1 ? 's' : ''} available
104 </div>
105 )}
106 {test.status === 'error' && (
107 <div className="settings-status error">{test.message}</div>
108 )}
109 </div>
110
111 <div className="settings-field">
112 <label className="settings-field-label">Model</label>
113 <input
114 className="settings-input"
115 value={model}
116 onChange={e => setModel(e.target.value)}
117 onBlur={() => commitModel(model)}
118 onKeyDown={e => e.key === 'Enter' && commitModel(model)}
119 placeholder="llama3.2"
120 spellCheck={false}
121 />
122 {test.status === 'ok' && test.models.length > 0 && (
123 <div className="settings-model-list">
124 {test.models.map(m => (
125 <button
126 key={m}
127 className={`settings-model-item${m === model ? ' active' : ''}`}
128 onClick={() => commitModel(m)}
129 >
130 {m}
131 </button>
132 ))}
133 </div>
134 )}
135 <div className="settings-field-hint">
136 Pull a model first: <code>ollama pull llama3.2</code>
137 </div>
138 </div>
139 </div>
140 )}
141
142 {/* Help */}
143 <div className="settings-section settings-help">
144 <div className="settings-section-label">About Ollama</div>
145 <p>
146 Ollama runs AI models locally on your machine — no API key, no data sent externally.
147 Install from <strong>ollama.com</strong>, then pull any model to get started.
148 </p>
149 <p>
150 Leave <strong>Server URL</strong> empty to route through the app's
151 own server (works automatically with the Docker setup). For local
152 dev, set it to <code>http://localhost:11434</code> and ensure CORS
153 is open: <code>OLLAMA_ORIGINS=* ollama serve</code>
154 </p>
155 </div>
156 </div>
157 </div>
158 </div>
159 );
160}