feat: configure Qwen endpoint per course
This commit is contained in:
@@ -117,6 +117,7 @@ export type CourseAiSetting = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
provider: 'OPENAI' | 'ANTHROPIC' | 'GEMINI' | 'QWEN';
|
provider: 'OPENAI' | 'ANTHROPIC' | 'GEMINI' | 'QWEN';
|
||||||
model: string;
|
model: string;
|
||||||
|
endpointUrl?: string | null;
|
||||||
hasApiKey: boolean;
|
hasApiKey: boolean;
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
};
|
};
|
||||||
@@ -154,7 +155,7 @@ export async function getCourseAiSetting(accessToken: string, courseId: string):
|
|||||||
export async function updateCourseAiSetting(
|
export async function updateCourseAiSetting(
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
courseId: string,
|
courseId: string,
|
||||||
input: Pick<CourseAiSetting, 'enabled' | 'provider' | 'model'> & { apiKey?: string },
|
input: Pick<CourseAiSetting, 'enabled' | 'provider' | 'model'> & { apiKey?: string; endpointUrl?: string },
|
||||||
): Promise<CourseAiSetting> {
|
): Promise<CourseAiSetting> {
|
||||||
const response = await fetch(`${LMS_API_BASE_URL}/instructor/courses/${encodeURIComponent(courseId)}/ai-setting`, {
|
const response = await fetch(`${LMS_API_BASE_URL}/instructor/courses/${encodeURIComponent(courseId)}/ai-setting`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const AI_MODEL_DEFAULTS: Record<CourseAiSetting['provider'], string> = {
|
|||||||
QWEN: 'qwen3.7-plus',
|
QWEN: 'qwen3.7-plus',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_QWEN_ENDPOINT = 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1';
|
||||||
|
|
||||||
const AI_PROVIDER_LABELS: Record<CourseAiSetting['provider'], string> = {
|
const AI_PROVIDER_LABELS: Record<CourseAiSetting['provider'], string> = {
|
||||||
OPENAI: 'OpenAI',
|
OPENAI: 'OpenAI',
|
||||||
ANTHROPIC: 'Claude',
|
ANTHROPIC: 'Claude',
|
||||||
@@ -187,6 +189,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
const [aiProvider, setAiProvider] = useState<CourseAiSetting['provider']>('OPENAI');
|
const [aiProvider, setAiProvider] = useState<CourseAiSetting['provider']>('OPENAI');
|
||||||
const [aiModel, setAiModel] = useState(AI_MODEL_DEFAULTS.OPENAI);
|
const [aiModel, setAiModel] = useState(AI_MODEL_DEFAULTS.OPENAI);
|
||||||
|
const [aiEndpointUrl, setAiEndpointUrl] = useState(DEFAULT_QWEN_ENDPOINT);
|
||||||
const [aiApiKey, setAiApiKey] = useState('');
|
const [aiApiKey, setAiApiKey] = useState('');
|
||||||
const [aiHasApiKey, setAiHasApiKey] = useState(false);
|
const [aiHasApiKey, setAiHasApiKey] = useState(false);
|
||||||
const [aiConfiguredProvider, setAiConfiguredProvider] = useState<CourseAiSetting['provider'] | null>(null);
|
const [aiConfiguredProvider, setAiConfiguredProvider] = useState<CourseAiSetting['provider'] | null>(null);
|
||||||
@@ -326,6 +329,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
setAiEnabled(setting.enabled);
|
setAiEnabled(setting.enabled);
|
||||||
setAiProvider(setting.provider);
|
setAiProvider(setting.provider);
|
||||||
setAiModel(setting.model);
|
setAiModel(setting.model);
|
||||||
|
setAiEndpointUrl(setting.endpointUrl || DEFAULT_QWEN_ENDPOINT);
|
||||||
setAiHasApiKey(setting.hasApiKey);
|
setAiHasApiKey(setting.hasApiKey);
|
||||||
setAiConfiguredProvider(setting.hasApiKey ? setting.provider : null);
|
setAiConfiguredProvider(setting.hasApiKey ? setting.provider : null);
|
||||||
setAiApiKey('');
|
setAiApiKey('');
|
||||||
@@ -348,11 +352,13 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
enabled: aiEnabled,
|
enabled: aiEnabled,
|
||||||
provider: aiProvider,
|
provider: aiProvider,
|
||||||
model: aiModel.trim(),
|
model: aiModel.trim(),
|
||||||
|
...(aiProvider === 'QWEN' ? { endpointUrl: aiEndpointUrl.trim() } : {}),
|
||||||
...(aiApiKey.trim() ? { apiKey: aiApiKey.trim() } : {}),
|
...(aiApiKey.trim() ? { apiKey: aiApiKey.trim() } : {}),
|
||||||
});
|
});
|
||||||
setAiEnabled(setting.enabled);
|
setAiEnabled(setting.enabled);
|
||||||
setAiProvider(setting.provider);
|
setAiProvider(setting.provider);
|
||||||
setAiModel(setting.model);
|
setAiModel(setting.model);
|
||||||
|
setAiEndpointUrl(setting.endpointUrl || DEFAULT_QWEN_ENDPOINT);
|
||||||
setAiHasApiKey(setting.hasApiKey);
|
setAiHasApiKey(setting.hasApiKey);
|
||||||
setAiConfiguredProvider(setting.hasApiKey ? setting.provider : null);
|
setAiConfiguredProvider(setting.hasApiKey ? setting.provider : null);
|
||||||
setAiApiKey('');
|
setAiApiKey('');
|
||||||
@@ -1317,6 +1323,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAiProvider(provider);
|
setAiProvider(provider);
|
||||||
setAiModel(AI_MODEL_DEFAULTS[provider]);
|
setAiModel(AI_MODEL_DEFAULTS[provider]);
|
||||||
|
if (provider === 'QWEN' && !aiEndpointUrl.trim()) setAiEndpointUrl(DEFAULT_QWEN_ENDPOINT);
|
||||||
setAiApiKey('');
|
setAiApiKey('');
|
||||||
}}
|
}}
|
||||||
className={`h-10 rounded-md border text-[13px] font-black transition disabled:cursor-not-allowed disabled:opacity-60 ${
|
className={`h-10 rounded-md border text-[13px] font-black transition disabled:cursor-not-allowed disabled:opacity-60 ${
|
||||||
@@ -1343,6 +1350,19 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{aiProvider === 'QWEN' ? (
|
||||||
|
<label className="mt-4 block">
|
||||||
|
<span className="text-[12px] font-black text-[#475569]">엔드포인트 주소</span>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={aiEndpointUrl}
|
||||||
|
onChange={(event) => setAiEndpointUrl(event.target.value)}
|
||||||
|
placeholder={DEFAULT_QWEN_ENDPOINT}
|
||||||
|
className="mt-1.5 h-10 w-full rounded-md border border-[#d1d9e6] bg-white px-3 font-mono text-[12px] font-bold text-[#111827] outline-none focus:border-[#2563eb]"
|
||||||
|
/>
|
||||||
|
<span className="mt-1.5 block text-[11px] font-semibold text-[#64748b]">OpenAI 호환 API의 기본 주소를 입력하세요. HTTPS 주소만 사용할 수 있습니다.</span>
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
<label className="mt-4 block">
|
<label className="mt-4 block">
|
||||||
<span className="flex items-center gap-1.5 text-[12px] font-black text-[#475569]"><KeyRound size={13} aria-hidden="true" />API 키</span>
|
<span className="flex items-center gap-1.5 text-[12px] font-black text-[#475569]"><KeyRound size={13} aria-hidden="true" />API 키</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user