import { useEffect, useRef, useState } from 'react'; import { Bot, KeyRound, Loader2, RefreshCw, Save } from 'lucide-react'; import { PageHeader } from '../../components/PageHeader'; import { useToast } from '../../components/ToastProvider'; import { getCourseAiSetting, updateCourseAiSetting, type CourseAiSetting } from '../../lib/curriculum'; import type { InstructorCourse } from '../../lib/courses'; import type { AuthSession } from '../../types/auth'; const AI_MODEL_DEFAULTS: Record = { A_COPILOT: 'a-copilot', OPENAI: 'gpt-6-astra', ANTHROPIC: 'claude-fable-5-1', GEMINI: 'gemini-4-argon', QWEN: 'qwen3.8-max', }; const DEFAULT_QWEN_ENDPOINT = 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1'; const CUSTOM_AI_MODEL = '__custom__'; const AI_PROVIDER_LABELS: Record = { A_COPILOT: 'A-Copilot', OPENAI: 'OpenAI', ANTHROPIC: 'Claude', GEMINI: 'Gemini', QWEN: 'Qwen', }; const AI_MODEL_OPTIONS: Record, Array<{ value: string; label: string }>> = { // Official provider model catalogs checked on 2026-10-19; keep existing selections intact. OPENAI: [ { value: 'gpt-6-astra', label: 'GPT-6 Astra' }, { value: 'gpt-6.1-sol', label: 'GPT-6.1 Sol' }, { value: 'gpt-6-luna', label: 'GPT-6 Luna' }, { value: 'gpt-4.1-mini', label: 'GPT-4.1 Mini' }, { value: 'gpt-4.1', label: 'GPT-4.1' }, { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, ], ANTHROPIC: [ { value: 'claude-fable-5-1', label: 'Claude Fable 5.1' }, { value: 'claude-opus-5-5', label: 'Claude Opus 5.5' }, { value: 'claude-sonnet-5-5', label: 'Claude Sonnet 5.5' }, { value: 'claude-haiku-5-5', label: 'Claude Haiku 5.5' }, { value: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' }, { value: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' }, { value: 'claude-opus-4-1', label: 'Claude Opus 4.1' }, ], GEMINI: [ { value: 'gemini-4-argon', label: 'Gemini 4 Argon' }, { value: 'gemini-3.8-flash', label: 'Gemini 3.8 Flash' }, { value: 'gemini-3.5-flash-lite', label: 'Gemini 3.5 Flash-Lite' }, { value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro (Preview)' }, { value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' }, { value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' }, { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, ], QWEN: [ { value: 'qwen3.8-max', label: 'Qwen 3.8 Max' }, { value: 'qwen3.8-flash', label: 'Qwen 3.8 Flash' }, { value: 'qwen3.7-plus', label: 'Qwen 3.7 Plus' }, { value: 'qwen3.7-flash', label: 'Qwen 3.7 Flash' }, { value: 'qwen-vl-plus', label: 'Qwen VL Plus' }, { value: 'qwen-vl-max', label: 'Qwen VL Max' }, { value: 'qwen-plus', label: 'Qwen Plus (text)' }, ], }; type SettingsPageProps = { session: AuthSession; selectedCourseId: string; courses: InstructorCourse[]; }; export function SettingsPage({ session, selectedCourseId, courses }: SettingsPageProps) { const selectedCourse = courses.find(course => course.id === selectedCourseId); return (
{selectedCourse ? ( ) : (

설정할 강좌가 없습니다.

)}
); } function CourseAiSettings({ session, selectedCourseId }: Pick) { const { showToast } = useToast(); const [aiEnabled, setAiEnabled] = useState(false); const [aiProvider, setAiProvider] = useState('A_COPILOT'); const [aiModel, setAiModel] = useState(AI_MODEL_DEFAULTS.A_COPILOT); const [aiCustomModel, setAiCustomModel] = useState(false); const [aiEndpointUrl, setAiEndpointUrl] = useState(DEFAULT_QWEN_ENDPOINT); const [aiApiKey, setAiApiKey] = useState(''); const [aiHasApiKey, setAiHasApiKey] = useState(false); const [aiCopilotConfigured, setAiCopilotConfigured] = useState(false); const [aiConfiguredProvider, setAiConfiguredProvider] = useState(null); const [aiSettingLoading, setAiSettingLoading] = useState(true); const [saving, setSaving] = useState(false); const [loaded, setLoaded] = useState(false); const [loadAttempt, setLoadAttempt] = useState(0); const [error, setError] = useState(null); const mounted = useRef(false); const applySetting = (setting: CourseAiSetting) => { setAiEnabled(setting.enabled); setAiProvider(setting.provider); setAiModel(setting.provider === 'A_COPILOT' ? AI_MODEL_DEFAULTS.A_COPILOT : setting.model); setAiCustomModel(setting.provider !== 'A_COPILOT' && !AI_MODEL_OPTIONS[setting.provider].some(option => option.value === setting.model)); setAiEndpointUrl(setting.endpointUrl || DEFAULT_QWEN_ENDPOINT); setAiHasApiKey(setting.hasApiKey); setAiCopilotConfigured(setting.copilotConfigured === true); setAiConfiguredProvider(setting.hasApiKey ? setting.provider : null); setAiApiKey(''); }; useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); useEffect(() => { let cancelled = false; setAiSettingLoading(true); setLoaded(false); setError(null); getCourseAiSetting(session.accessToken, selectedCourseId) .then(setting => { if (cancelled) return; applySetting(setting); setLoaded(true); }) .catch((reason: Error) => { if (!cancelled) setError(reason.message); }) .finally(() => { if (!cancelled) setAiSettingLoading(false); }); return () => { cancelled = true; }; }, [session.accessToken, selectedCourseId, loadAttempt]); const handleSaveAiSetting = async () => { if (!loaded || aiSettingLoading || saving) return; if (aiProvider !== 'A_COPILOT' && (!aiModel.trim() || aiModel.trim().length > 120)) { setError('모델 ID는 1~120자로 입력해주세요.'); return; } setSaving(true); setError(null); try { const setting = await updateCourseAiSetting(session.accessToken, selectedCourseId, { enabled: aiEnabled, provider: aiProvider, ...(aiProvider !== 'A_COPILOT' ? { model: aiModel.trim() } : {}), ...(aiProvider === 'QWEN' ? { endpointUrl: aiEndpointUrl.trim() } : {}), ...(aiProvider !== 'A_COPILOT' && aiApiKey.trim() ? { apiKey: aiApiKey.trim() } : {}), }); if (!mounted.current) return; applySetting(setting); showToast({ title: 'AI 에이전트 설정이 저장되었습니다.', message: setting.enabled ? setting.provider === 'A_COPILOT' ? 'A-Copilot' : `${AI_PROVIDER_LABELS[setting.provider]} · ${setting.model}` : '사용 안 함' }); } catch (reason) { if (mounted.current) setError(reason instanceof Error ? reason.message : 'AI 에이전트 설정을 저장하지 못했습니다.'); } finally { if (mounted.current) setSaving(false); } }; return (
{ event.preventDefault(); void handleSaveAiSetting(); }} >
AI 에이전트

{aiSettingLoading ? (

) : loaded ? (
AI 공급자
{(Object.keys(AI_PROVIDER_LABELS) as CourseAiSetting['provider'][]).map((provider) => ( ))}
{aiProvider === 'A_COPILOT' ? (
{!aiCopilotConfigured ?

A-Copilot 서버 연결이 설정되어 있지 않습니다.

: null}
) : ( <> {aiCustomModel ? ( ) : null} {aiProvider === 'QWEN' ? ( ) : null} )}
) : null}
{error ? (
{error} {!loaded ? : null}
) : null}
); }