feat(cis): add LLM config modal, summary generation, keywords tab, and export button (Phase 3)

- LLMConfigModal: provider dropdown, model field, API key input (localStorage only)
- Summary generation: modal with 5 summary types, calls API, displays result
- Keywords tab: add/delete watches, real-time detection, event logging
- Export button: SRT/TXT/JSON download via API endpoint
- All Phase 3 frontend tasks complete
This commit is contained in:
2026-05-20 22:57:06 +00:00
parent 6bbed42f55
commit cb8c2dab74
3 changed files with 639 additions and 43 deletions
@@ -0,0 +1,126 @@
import React, { useState, useEffect } from 'react';
import Close from '@mui/icons-material/Close';
import { useTranscriptionStore, LLMConfig } from '../../stores/transcriptionStore';
const PROVIDERS = [
{ value: 'openai', label: 'OpenAI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'ollama', label: 'Ollama' },
{ value: 'openrouter', label: 'OpenRouter' },
{ value: 'google', label: 'Google' },
] as const;
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), 2000);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[9999] overflow-y-auto">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 transition-opacity"
onClick={onClose}
/>
{/* Modal panel */}
<div className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 w-full max-w-md mx-auto">
{/* Header */}
<div className="bg-gray-50 px-4 py-3 flex items-center justify-between border-b border-gray-200">
<h3 className="text-lg font-medium leading-6 text-gray-900">
LLM Provider Settings
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-500 focus:outline-none"
>
<Close sx={{ fontSize: 20 }} />
</button>
</div>
{/* Content */}
<div className="px-4 py-5 sm:p-6 space-y-4">
{/* Provider dropdown */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Provider
</label>
<select
value={form.provider}
onChange={(e) => setForm({ ...form, provider: e.target.value as LLMConfig['provider'] })}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{PROVIDERS.map((p) => (
<option key={p.value} value={p.value}>
{p.label}
</option>
))}
</select>
</div>
{/* Model name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Model
</label>
<input
type="text"
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="e.g. gpt-4o, claude-sonnet-4-20250514"
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* API Key */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
API Key
</label>
<input
type="password"
value={form.apiKey}
onChange={(e) => setForm({ ...form, apiKey: e.target.value })}
placeholder="sk-..."
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Note */}
<p className="text-xs text-gray-500">
API keys are stored locally in your browser only. They are never sent to Supabase or stored on any server.
</p>
{/* Save button */}
<button
onClick={handleSave}
className={`w-full rounded-md px-4 py-2 text-sm font-medium text-white transition-colors ${
saved
? 'bg-green-600 hover:bg-green-700'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{saved ? '✓ Saved!' : 'Save Settings'}
</button>
</div>
</div>
</div>
);
};
export default LLMConfigModal;