mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-02 01:15:18 +08:00
Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Plus } from "lucide-react";
|
|
import { useI18n } from "@/lib/use-i18n";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export interface NewProviderData {
|
|
name: string;
|
|
type: "openai" | "anthropic" | "google";
|
|
baseUrl: string;
|
|
icon: string;
|
|
requiresApiKey: boolean;
|
|
}
|
|
|
|
interface AddProviderDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onAdd: (provider: NewProviderData) => void;
|
|
}
|
|
|
|
export function AddProviderDialog({
|
|
open,
|
|
onOpenChange,
|
|
onAdd,
|
|
}: AddProviderDialogProps) {
|
|
const { t } = useI18n();
|
|
|
|
// Internal state
|
|
const [name, setName] = useState("");
|
|
const [type, setType] = useState<"openai" | "anthropic" | "google">("openai");
|
|
const [baseUrl, setBaseUrl] = useState("");
|
|
const [icon, setIcon] = useState("");
|
|
const [requiresApiKey, setRequiresApiKey] = useState(true);
|
|
|
|
// Reset form when dialog closes (derived state pattern)
|
|
const [prevOpen, setPrevOpen] = useState(open);
|
|
if (open !== prevOpen) {
|
|
setPrevOpen(open);
|
|
if (!open) {
|
|
setName("");
|
|
setType("openai");
|
|
setBaseUrl("");
|
|
setIcon("");
|
|
setRequiresApiKey(true);
|
|
}
|
|
}
|
|
|
|
const handleClose = () => {
|
|
onOpenChange(false);
|
|
};
|
|
|
|
const handleAdd = () => {
|
|
onAdd({
|
|
name,
|
|
type,
|
|
baseUrl,
|
|
icon,
|
|
requiresApiKey,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[450px]">
|
|
<DialogTitle className="sr-only">
|
|
{t("settings.addProviderDialog")}
|
|
</DialogTitle>
|
|
<DialogDescription className="sr-only">
|
|
{t("settings.addProviderDescription")}
|
|
</DialogDescription>
|
|
<div className="space-y-4">
|
|
<div className="pb-3 border-b">
|
|
<h2 className="text-lg font-semibold">
|
|
{t("settings.addProviderDialog")}
|
|
</h2>
|
|
</div>
|
|
|
|
{/* Provider Name */}
|
|
<div className="space-y-2">
|
|
<Label>{t("settings.providerName")}</Label>
|
|
<Input
|
|
placeholder={t("settings.providerNamePlaceholder")}
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* API Mode */}
|
|
<div className="space-y-2">
|
|
<Label>{t("settings.providerApiMode")}</Label>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<button
|
|
onClick={() => setType("openai")}
|
|
className={cn(
|
|
"p-2 rounded-lg border text-left text-sm transition-colors",
|
|
type === "openai"
|
|
? "bg-primary/5 border-primary/50"
|
|
: "hover:bg-muted/50 border-transparent"
|
|
)}
|
|
>
|
|
{t("settings.apiModeOpenAI")}
|
|
</button>
|
|
<button
|
|
onClick={() => setType("anthropic")}
|
|
className={cn(
|
|
"p-2 rounded-lg border text-left text-sm transition-colors",
|
|
type === "anthropic"
|
|
? "bg-primary/5 border-primary/50"
|
|
: "hover:bg-muted/50 border-transparent"
|
|
)}
|
|
>
|
|
{t("settings.apiModeAnthropic")}
|
|
</button>
|
|
<button
|
|
onClick={() => setType("google")}
|
|
className={cn(
|
|
"p-2 rounded-lg border text-left text-sm transition-colors",
|
|
type === "google"
|
|
? "bg-primary/5 border-primary/50"
|
|
: "hover:bg-muted/50 border-transparent"
|
|
)}
|
|
>
|
|
{t("settings.apiModeGoogle")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Default Base URL */}
|
|
<div className="space-y-2">
|
|
<Label>{t("settings.defaultBaseUrl")}</Label>
|
|
<Input
|
|
type="url"
|
|
placeholder="https://api.example.com/v1"
|
|
value={baseUrl}
|
|
onChange={(e) => setBaseUrl(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Icon URL */}
|
|
<div className="space-y-2">
|
|
<Label>{t("settings.providerIcon")}</Label>
|
|
<Input
|
|
type="url"
|
|
placeholder="https://example.com/icon.svg"
|
|
value={icon}
|
|
onChange={(e) => setIcon(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Requires API Key */}
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="requires-api-key"
|
|
checked={requiresApiKey}
|
|
onCheckedChange={(checked) => setRequiresApiKey(checked as boolean)}
|
|
/>
|
|
<label
|
|
htmlFor="requires-api-key"
|
|
className="text-sm cursor-pointer"
|
|
>
|
|
{t("settings.requiresApiKey")}
|
|
</label>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-2 pt-3 border-t">
|
|
<Button variant="outline" size="sm" onClick={handleClose}>
|
|
{t("settings.cancelEdit")}
|
|
</Button>
|
|
<Button size="sm" onClick={handleAdd} className="gap-1.5">
|
|
<Plus className="h-3.5 w-3.5" />
|
|
{t("settings.addProviderButton")}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|