Files
app/src/utils/tldraw/ui-overrides/components/shared/LLMConfigModal.tsx
T
kcarandClaude Sonnet 4.6 b0c7758135 feat(phase-b): Supabase navigation store, snapshot service, auth wiring
navigationStore: rewritten off Neo4j db names — Supabase whiteboard_rooms table,
  setAuthInfo(token, userId) pattern, auto-creates default room per context on first use
snapshotService: rewritten to Supabase Storage REST (/storage/v1/object/authenticated/cc.users/…),
  setAccessToken() instance method, static methods take accessToken not dbName
AuthContext/NeoUserContext: auth injected into nav store, no Neo4j db names required
singlePlayerPage: loadNodeData no longer calls Neo4j; snapshot wired via accessToken
navigation types: NeoGraphNode updated for Supabase-backed tree structure
transcriptionStore/Service: getSession() removed, accessToken via AuthContext
LLMConfigModal: auth context wiring fixes
GraphNavigator/GraphSidebar: updated nav components

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-26 01:25:15 +01:00

260 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { useTranscriptionStore, LLMConfig } from '../../../../../stores/transcriptionStore';
const PROVIDERS = [
{ value: 'openai', label: 'OpenAI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'ollama', label: 'Ollama (local)' },
{ value: 'openrouter', label: 'OpenRouter' },
{ value: 'google', label: 'Google Gemini' },
] as const;
const WHISPER_MODELS = [
{ value: 'tiny', label: 'Tiny (fastest, least accurate)' },
{ value: 'tiny.en', label: 'Tiny English' },
{ value: 'base', label: 'Base' },
{ value: 'base.en', label: 'Base English' },
{ value: 'small', label: 'Small' },
{ value: 'small.en', label: 'Small English' },
{ value: 'medium', label: 'Medium' },
{ value: 'medium.en', label: 'Medium English' },
{ value: 'large-v2', label: 'Large v2' },
{ value: 'large-v3', label: 'Large v3 (best accuracy)' },
];
const fieldStyle: React.CSSProperties = {
width: '100%',
padding: '7px 10px',
border: '1px solid var(--color-divider)',
borderRadius: '6px',
backgroundColor: 'var(--color-muted)',
color: 'var(--color-text)',
fontSize: '13px',
outline: 'none',
boxSizing: 'border-box',
};
const labelStyle: React.CSSProperties = {
display: 'block',
fontSize: '12px',
fontWeight: 600,
color: 'var(--color-text-2)',
marginBottom: '4px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
};
const LLMConfigModal: React.FC<{ isOpen: boolean; onClose: () => void }> = ({ isOpen, onClose }) => {
const { llmConfig, setLLMConfig } = useTranscriptionStore();
const [form, setForm] = useState<LLMConfig>(llmConfig);
const [saved, setSaved] = useState(false);
useEffect(() => {
if (isOpen) {
setForm(useTranscriptionStore.getState().llmConfig);
setSaved(false);
}
}, [isOpen]);
const handleSave = () => {
setLLMConfig(form);
setSaved(true);
setTimeout(() => {
setSaved(false);
onClose();
}, 1000);
};
if (!isOpen) return null;
return (
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 99999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
{/* Backdrop */}
<div style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(0,0,0,0.5)' }} />
{/* Modal panel */}
<div
style={{
position: 'relative',
width: '100%',
maxWidth: '420px',
backgroundColor: 'var(--color-panel)',
border: '1px solid var(--color-divider)',
borderRadius: '10px',
boxShadow: '0 20px 60px rgba(0,0,0,0.4)',
overflow: 'hidden',
zIndex: 1,
}}
onMouseDown={(e) => e.stopPropagation()}
>
{/* Header */}
<div style={{
padding: '14px 16px',
borderBottom: '1px solid var(--color-divider)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<span style={{ fontSize: '14px', fontWeight: 600, color: 'var(--color-text)' }}>
Settings
</span>
<button
onClick={onClose}
style={{
padding: '2px 6px',
border: 'none',
backgroundColor: 'transparent',
color: 'var(--color-text-2)',
cursor: 'pointer',
fontSize: '18px',
lineHeight: 1,
}}
>
×
</button>
</div>
{/* Content */}
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: '14px', maxHeight: '80vh', overflowY: 'auto' }}>
{/* ── Transcription section ── */}
<div>
<div style={{
fontSize: '11px',
fontWeight: 700,
color: 'var(--color-text-3)',
textTransform: 'uppercase',
letterSpacing: '0.08em',
marginBottom: '10px',
paddingBottom: '6px',
borderBottom: '1px solid var(--color-divider)',
}}>
Transcription
</div>
<label style={labelStyle}>Whisper Model</label>
<select
value={form.whisperModel || 'large-v3'}
onChange={(e) => setForm({ ...form, whisperModel: e.target.value })}
style={fieldStyle}
>
{WHISPER_MODELS.map((m) => (
<option key={m.value} value={m.value}>{m.label}</option>
))}
</select>
<div style={{ fontSize: '11px', color: 'var(--color-text-3)', marginTop: '4px' }}>
Larger models are more accurate but slower to load. Server has large-v3 downloaded.
</div>
</div>
{/* ── LLM section ── */}
<div>
<div style={{
fontSize: '11px',
fontWeight: 700,
color: 'var(--color-text-3)',
textTransform: 'uppercase',
letterSpacing: '0.08em',
marginBottom: '10px',
paddingBottom: '6px',
borderBottom: '1px solid var(--color-divider)',
}}>
AI Summary Provider
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div>
<label style={labelStyle}>Provider</label>
<select
value={form.provider}
onChange={(e) => setForm({ ...form, provider: e.target.value as LLMConfig['provider'] })}
style={fieldStyle}
>
{PROVIDERS.map((p) => (
<option key={p.value} value={p.value}>{p.label}</option>
))}
</select>
</div>
<div>
<label style={labelStyle}>Model</label>
<input
type="text"
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder={
form.provider === 'ollama' ? 'e.g. gemma4:e4b, llama3.2' :
form.provider === 'anthropic' ? 'e.g. claude-sonnet-4-6' :
form.provider === 'google' ? 'e.g. gemini-2.0-flash' :
'e.g. gpt-4o, gpt-4o-mini'
}
style={fieldStyle}
/>
</div>
{form.provider === 'ollama' && (
<div>
<label style={labelStyle}>Ollama Base URL</label>
<input
type="text"
value={form.baseUrl || ''}
onChange={(e) => setForm({ ...form, baseUrl: e.target.value })}
placeholder="https://ollama.kevlarai.com"
style={fieldStyle}
/>
</div>
)}
<div>
<label style={labelStyle}>
{form.provider === 'ollama' ? 'API Key (optional — leave blank if unrestricted)' : 'API Key'}
</label>
<input
type="password"
value={form.apiKey}
onChange={(e) => setForm({ ...form, apiKey: e.target.value })}
placeholder={form.provider === 'ollama' ? 'Leave blank or any value' : 'sk-...'}
style={fieldStyle}
/>
</div>
</div>
<div style={{ fontSize: '11px', color: 'var(--color-text-3)', marginTop: '8px' }}>
API keys are stored in your browser only.
</div>
</div>
{/* Save button */}
<button
onClick={handleSave}
style={{
padding: '9px',
border: 'none',
borderRadius: '6px',
backgroundColor: saved ? '#16a34a' : '#2563eb',
color: '#fff',
fontSize: '13px',
fontWeight: 600,
cursor: 'pointer',
transition: 'background-color 200ms',
}}
>
{saved ? '✓ Saved' : 'Save Settings'}
</button>
</div>
</div>
</div>
);
};
export default LLMConfigModal;