// frontend/components/settings/LlmTab.tsx — LLM 연결(provider/model/host/timeout) 편집·테스트·적용 "use client"; import { useEffect, useState } from "react"; import { Button } from "@/components/Button"; import { Icon } from "@/components/Icon"; import { SegmentToggle } from "@/components/SegmentToggle"; import { settingsApi } from "@/lib/settings/api"; import { useLlmConfig } from "@/lib/hooks/useSettings"; import type { LlmProvider, LlmTestResult } from "@/lib/types"; import type { ToastFn } from "./SettingsClient"; const PROVIDER_LABEL: Record = { auto: "자동", ollama: "Ollama", heuristic: "규칙(오프라인)", }; export function LlmTab({ toast }: { toast: ToastFn }) { const { data: cfg, isLoading, refresh } = useLlmConfig(); const [provider, setProvider] = useState("ollama"); const [model, setModel] = useState(""); const [host, setHost] = useState(""); const [timeout, setTimeoutVal] = useState(""); const [test, setTest] = useState(null); const [testing, setTesting] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (cfg) { setProvider(cfg.provider); setModel(cfg.model); setHost(cfg.host); setTimeoutVal(String(cfg.timeout)); } }, [cfg]); const isHeuristic = provider === "heuristic"; const onTest = async () => { setTesting(true); setTest(null); try { const r = await settingsApi.testLlm({ provider, host, model }); setTest(r); toast( r.reachable ? "연결에 성공했어요" : "연결할 수 없어요", r.reachable ? "green" : "coral", ); } catch { toast("연결 테스트에 실패했어요", "coral"); } finally { setTesting(false); } }; const onSave = async () => { setSaving(true); try { const t = parseFloat(timeout); await settingsApi.updateLlm({ provider, model, host, timeout: Number.isFinite(t) && t > 0 ? t : undefined, }); await refresh(); toast("LLM 설정을 저장했어요", "green"); } catch { toast("저장에 실패했어요", "coral"); } finally { setSaving(false); } }; const onReset = async () => { setSaving(true); try { await settingsApi.updateLlm({ provider: "", model: "", host: "", timeout: 0 }); const next = await refresh(); if (next) { setProvider(next.provider); setModel(next.model); setHost(next.host); setTimeoutVal(String(next.timeout)); } setTest(null); toast("기본값(.env)으로 되돌렸어요", "blue"); } catch { toast("초기화에 실패했어요", "coral"); } finally { setSaving(false); } }; return (

AI · LLM 연결

아리의 분류·요약을 담당하는 모델 연결이에요. 저장하면 즉시 적용돼요.

{cfg?.overridden && 사용자 설정 적용 중}
({ id: p, label: PROVIDER_LABEL[p as LlmProvider] ?? p, }))} value={provider} onChange={(id) => setProvider(id as LlmProvider)} /> 로컬 Ollama 서버를 사용해요.
setHost(e.target.value)} placeholder="http://localhost:11434" />
setModel(e.target.value)} placeholder="예: llama3.1" /> {test && test.models.length > 0 && (
{test.models.map((m) => ( ))}
)}
setTimeoutVal(e.target.value)} />
{cfg?.overridden && ( )}
{test && (
{test.reachable ? `연결됨 · ${test.provider}${test.model ? " · " + test.model : ""}` : `연결 실패 · ${test.detail || "응답 없음"}`}
)}

보조 프로바이더

환경변수(.env)로 관리 · 읽기 전용
임베딩(RAG)
{cfg?.embed_provider ?? "—"} {cfg?.embed_model ? ` · ${cfg.embed_model}` : ""}
에이전트
{cfg?.agent_provider ?? "—"}
음성 인식(STT)
{cfg?.stt_provider ?? "—"}
이미지 인식(Vision)
{cfg?.vision_provider ?? "—"}
); }