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}