grapholab / frontend /src /components /settings /SettingsDialog.tsx
Fabio Antonini
feat: move language toggle into Settings dialog
13c1698
import { useTranslation } from "react-i18next"
import { RefreshCw, Check, Trash2 } from "lucide-react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
interface SettingsDialogProps {
open: boolean
onClose: () => void
// Language
currentLang: string
onToggleLang: () => void
// Ollama + OpenAI models
models: string[]
ollamaUp: boolean | null
refreshing: boolean
onRefresh: () => void
openaiModels: { llm: string[]; vlm: string[]; embed: string[] }
// LLM model
currentModel: string
onModelChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
// OCR model
currentOcrModel: string
onOcrModelChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
// VLM model
currentVlmModel: string
onVlmModelChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
// Embedding model
openaiKeyConfigured: boolean
currentEmbedModel: string
onEmbedModelChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
// OpenAI key
showKeyInput: boolean
setShowKeyInput: (v: boolean) => void
keyDraft: string
setKeyDraft: (v: string) => void
savingKey: boolean
keyError: string
onSaveKey: (e: React.FormEvent) => void
onDeleteKey: () => void
}
const OCR_MODELS = ["easyocr", "vlm", "paddleocr", "trocr"]
export default function SettingsDialog({
open,
onClose,
models,
ollamaUp,
refreshing,
onRefresh,
openaiModels,
currentModel,
onModelChange,
currentOcrModel,
onOcrModelChange,
currentVlmModel,
onVlmModelChange,
openaiKeyConfigured,
currentEmbedModel,
onEmbedModelChange,
showKeyInput,
setShowKeyInput,
keyDraft,
setKeyDraft,
savingKey,
keyError,
onSaveKey,
onDeleteKey,
currentLang,
onToggleLang,
}: SettingsDialogProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={(v) => { if (!v) onClose() }}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="flex items-center justify-between pr-6">
<DialogTitle>{t("nav.settings")}</DialogTitle>
<button
onClick={onRefresh}
disabled={refreshing}
title={t("config.refresh")}
className="text-muted-foreground hover:text-foreground disabled:opacity-40 transition-colors"
>
<RefreshCw className={`h-3.5 w-3.5 ${refreshing ? "animate-spin" : ""}`} />
</button>
</div>
</DialogHeader>
<div className="px-6 pb-6 pt-4 space-y-4">
{/* LLM model */}
<div>
<label className="block text-sm font-medium mb-1.5">{t("config.model_label")}</label>
{ollamaUp === false && openaiModels.llm.length === 0 ? (
<p className="text-xs text-destructive">{t("config.model_offline")}</p>
) : models.length === 0 && openaiModels.llm.length === 0 ? (
<p className="text-xs text-muted-foreground">{t("config.model_loading")}</p>
) : (
<select
value={currentModel}
onChange={onModelChange}
className="w-full rounded-md border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
{models.length > 0 && (
<optgroup label="Ollama">
{models.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
)}
{openaiModels.llm.length > 0 && (
<optgroup label="OpenAI">
{openaiModels.llm.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
)}
</select>
)}
</div>
{/* OCR model */}
<div>
<label className="block text-sm font-medium mb-1.5">{t("config.ocr_model_label")}</label>
<select
value={currentOcrModel}
onChange={onOcrModelChange}
className="w-full rounded-md border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
{OCR_MODELS.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
{/* VLM model */}
<div>
<label className="block text-sm font-medium mb-1.5">{t("config.vlm_model_label")}</label>
{models.length === 0 && openaiModels.vlm.length === 0 ? (
<p className="text-xs text-muted-foreground">{t("config.model_loading")}</p>
) : (
<select
value={currentVlmModel}
onChange={onVlmModelChange}
className="w-full rounded-md border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
{models.length > 0 && (
<optgroup label="Ollama">
{models.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
)}
{openaiModels.vlm.length > 0 && (
<optgroup label="OpenAI">
{openaiModels.vlm.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
)}
</select>
)}
</div>
{/* Embedding model (only when OpenAI key configured) */}
{openaiKeyConfigured && (
<div>
<label className="block text-sm font-medium mb-1.5">{t("config.embed_model_label")}</label>
<select
value={currentEmbedModel}
onChange={onEmbedModelChange}
className="w-full rounded-md border bg-background px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
>
<optgroup label="Ollama">
<option value="nomic-embed-text">nomic-embed-text</option>
</optgroup>
<optgroup label="OpenAI">
{openaiModels.embed.map(m => <option key={m} value={m}>{m}</option>)}
</optgroup>
</select>
</div>
)}
{/* OpenAI API key */}
<div className="pt-2 border-t">
<label className="block text-sm font-medium mb-1.5">{t("config.openai_key_label")}</label>
{openaiKeyConfigured && !showKeyInput ? (
<div className="flex items-center gap-2">
<span className="text-sm text-green-600 dark:text-green-400">{t("config.openai_key_ok")}</span>
<button
className="text-xs text-muted-foreground underline"
onClick={() => setShowKeyInput(true)}
>
{t("config.openai_key_change")}
</button>
<button
className="text-xs text-destructive hover:opacity-70 transition-opacity ml-auto"
onClick={onDeleteKey}
title={t("config.openai_key_remove")}
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
) : !showKeyInput ? (
<div>
<p className="text-xs text-muted-foreground mb-1">{t("config.openai_key_missing")}</p>
<button
className="text-sm text-primary underline"
onClick={() => setShowKeyInput(true)}
>
{t("config.openai_key_add")}
</button>
</div>
) : null}
{showKeyInput && (
<form onSubmit={onSaveKey} className="flex gap-1.5 mt-1">
<input
type="password"
value={keyDraft}
onChange={e => { setKeyDraft(e.target.value) }}
placeholder="sk-…"
className="flex-1 min-w-0 rounded border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
/>
<Button
type="submit"
size="icon"
className="h-7 w-7 shrink-0"
disabled={savingKey || !keyDraft.trim()}
>
<Check className="h-3.5 w-3.5" />
</Button>
</form>
)}
{keyError && <p className="text-xs text-destructive mt-1">{keyError}</p>}
</div>
{/* Language */}
<div className="pt-2 border-t flex items-center justify-between">
<label className="text-sm font-medium">{t("config.language_label")}</label>
<button
onClick={onToggleLang}
className="text-sm text-primary underline"
>
{currentLang === "it" ? "English" : "Italiano"}
</button>
</div>
</div>
</DialogContent>
</Dialog>
)
}