import { useEffect, useState } from 'react'; import { type MoaModelSettingsSnapshot, type MoaReferenceStatus, type MoaSettingsSnapshot, checkMoaModel, fetchMoaSettings, updateMoaSettings, } from './api'; export function MoaSettingsPanel() { const [config, setConfig] = useState(null); const [draft, setDraft] = useState(null); const [apiKeys, setApiKeys] = useState>({}); const [busy, setBusy] = useState(false); const [checking, setChecking] = useState(null); const [error, setError] = useState(null); const [savedAt, setSavedAt] = useState(null); useEffect(() => { let cancelled = false; setBusy(true); fetchMoaSettings() .then((next) => { if (cancelled) return; setConfig(next); setDraft(next); setError(null); }) .catch((err) => { if (cancelled) return; setError(err instanceof Error ? err.message : String(err)); }) .finally(() => { if (!cancelled) setBusy(false); }); return () => { cancelled = true; }; }, []); function setModel(name: string, patch: Partial) { setDraft((prev) => prev ? { ...prev, models: prev.models.map((model) => model.name === name ? { ...model, ...patch } : model, ), } : prev, ); } function updateStatus(name: string, status: MoaReferenceStatus) { const update = (prev: MoaSettingsSnapshot | null) => prev ? { ...prev, models: prev.models.map((model) => model.name === name ? { ...model, lastStatus: status } : model, ), } : prev; setConfig(update); setDraft(update); } async function save() { if (!draft) return; setBusy(true); setError(null); try { const next = await updateMoaSettings({ enabled: draft.enabled, models: draft.models.map((model) => ({ name: model.name, enabled: model.enabled, model: model.model, baseUrl: model.baseUrl, apiFormat: model.apiFormat, apiKey: apiKeys[model.name]?.trim() || undefined, })), }); setConfig(next); setDraft(next); setApiKeys({}); setSavedAt(Date.now()); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function testModel(name: string) { setChecking(name); setError(null); try { updateStatus(name, await checkMoaModel(name)); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setChecking(null); } } const dirty = draft !== null && config !== null && (JSON.stringify(draft) !== JSON.stringify(config) || Object.values(apiKeys).some((value) => value.trim())); return (
Arbiter references

MoA 참조 모델

Kimi, GLM 같은 외부 참조 모델을 켜고 연결 상태를 바로 확인합니다.

{error ?

{error}

: null} {!draft ? (

{busy ? '불러오는 중…' : 'MoA 설정 없음'}

) : ( setApiKeys((prev) => ({ ...prev, [name]: value })) } onModelChange={setModel} onSave={save} onTest={testModel} onToggle={() => setDraft((prev) => prev ? { ...prev, enabled: !prev.enabled } : prev, ) } savedAt={savedAt} /> )}
); } function MoaSettingsContent({ apiKeys, busy, checking, dirty, draft, onApiKeyChange, onModelChange, onSave, onTest, onToggle, savedAt, }: { apiKeys: Record; busy: boolean; checking: string | null; dirty: boolean; draft: MoaSettingsSnapshot; onApiKeyChange: (name: string, value: string) => void; onModelChange: ( name: string, patch: Partial, ) => void; onSave: () => Promise; onTest: (name: string) => Promise; onToggle: () => void; savedAt: number | null; }) { return ( <>
    {draft.models.map((model) => ( ))}
); } function formatMoaStatus(status: MoaReferenceStatus | null): string { if (!status) return '연결 테스트 전'; const at = new Date(status.checkedAt); const checkedAt = Number.isNaN(at.getTime()) ? status.checkedAt : at.toLocaleString('ko-KR'); if (status.ok) return `정상 · ${checkedAt}`; return `실패 · ${checkedAt} · ${status.error ?? 'unknown error'}`; } function MoaMasterToggle({ busy, enabled, onToggle, }: { busy: boolean; enabled: boolean; onToggle: () => void; }) { return ( ); } function MoaSettingsActions({ busy, checking, dirty, onSave, savedAt, }: { busy: boolean; checking: string | null; dirty: boolean; onSave: () => Promise; savedAt: number | null; }) { return (
{savedAt && !dirty ? ( 저장됨. 적용하려면 상단의 스택 재시작을 눌러 주세요. ) : null}
); } function MoaModelRow({ apiKeyValue, busy, checking, dirty, model, onApiKeyChange, onModelChange, onTest, }: { apiKeyValue: string; busy: boolean; checking: string | null; dirty: boolean; model: MoaModelSettingsSnapshot; onApiKeyChange: (name: string, value: string) => void; onModelChange: ( name: string, patch: Partial, ) => void; onTest: (name: string) => Promise; }) { return (
  • onModelChange(model.name, { model: e.target.value })} placeholder="model" type="text" value={model.model} /> onModelChange(model.name, { baseUrl: e.target.value }) } placeholder="base URL" type="text" value={model.baseUrl} /> onApiKeyChange(model.name, e.target.value)} placeholder={model.apiKeyConfigured ? 'API key set' : 'new API key'} type="password" value={apiKeyValue} /> {formatMoaStatus(model.lastStatus)}
  • ); }