ClassLens-2 / chatkit /frontend /src /components /step2 /PromptEditor.tsx
chih.yikuan
Redesign ClassLens v2: 3-step exam analysis workflow
bc45e54
Raw
History Blame Contribute Delete
5.79 kB
import { useState, useEffect } from "react";
import { apiGet, apiPost } from "../../lib/api";
import { ModelSelector } from "../ModelSelector";
interface Prompt {
id: number;
name: string;
content: string;
user_id: number;
author_email?: string;
}
interface PromptEditorProps {
promptContent: string;
onPromptChange: (content: string) => void;
sessionId: number;
onRunSummary: (model: string) => void;
isGenerating: boolean;
}
export function PromptEditor({
promptContent,
onPromptChange,
sessionId,
onRunSummary,
isGenerating,
}: PromptEditorProps) {
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [allPrompts, setAllPrompts] = useState<Prompt[]>([]);
const [saveName, setSaveName] = useState("");
const [showSave, setShowSave] = useState(false);
const [showOthers, setShowOthers] = useState(false);
const [saving, setSaving] = useState(false);
const [model, setModel] = useState("gpt-5.4");
useEffect(() => {
loadPrompts();
}, []);
const loadPrompts = async () => {
try {
const [mine, all] = await Promise.all([
apiGet<{ prompts: Prompt[] }>("/api/prompts"),
apiGet<{ prompts: Prompt[] }>("/api/prompts/all"),
]);
setPrompts(mine.prompts);
setAllPrompts(all.prompts);
} catch {}
};
const handleSave = async () => {
if (!saveName.trim()) return;
setSaving(true);
try {
await apiPost("/api/prompts", { name: saveName, content: promptContent });
setSaveName("");
setShowSave(false);
await loadPrompts();
} catch {}
setSaving(false);
};
const handleLoadPrompt = (prompt: Prompt) => {
onPromptChange(prompt.content);
setShowOthers(false);
};
return (
<div className="space-y-4">
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-3">
{/* Load own prompts */}
{prompts.length > 0 && (
<select
onChange={(e) => {
const p = prompts.find((p) => p.id === Number(e.target.value));
if (p) onPromptChange(p.content);
}}
defaultValue=""
className="input !w-auto !py-2 text-sm"
>
<option value="" disabled>我的提示詞...</option>
{prompts.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
)}
{/* Load others' prompts */}
<button
onClick={() => setShowOthers(!showOthers)}
className="btn btn-outline text-sm !py-2"
>
瀏覽其他人的提示詞
</button>
{/* Save button */}
<button
onClick={() => setShowSave(!showSave)}
className="btn btn-outline text-sm !py-2"
>
儲存提示詞
</button>
{/* Model selector - right aligned */}
<div className="ml-auto">
<ModelSelector value={model} onChange={setModel} label="報告模型:" />
</div>
</div>
{/* Others' prompts dropdown */}
{showOthers && (
<div className="card p-4 max-h-48 overflow-y-auto">
<h4 className="text-sm font-semibold text-[var(--color-text-muted)] mb-2">所有提示詞</h4>
{allPrompts.length === 0 ? (
<p className="text-sm text-[var(--color-text-muted)]">目前沒有其他人的提示詞</p>
) : (
<div className="space-y-2">
{allPrompts.map((p) => (
<button
key={p.id}
onClick={() => handleLoadPrompt(p)}
className="w-full text-left p-2 rounded-lg hover:bg-[var(--color-primary)]/10 transition-colors"
>
<span className="text-sm text-[var(--color-text)]">{p.name}</span>
{p.author_email && (
<span className="text-xs text-[var(--color-text-muted)] ml-2">
by {p.author_email}
</span>
)}
</button>
))}
</div>
)}
</div>
)}
{/* Save prompt form */}
{showSave && (
<div className="card p-4 flex items-center gap-3">
<input
type="text"
value={saveName}
onChange={(e) => setSaveName(e.target.value)}
placeholder="提示詞名稱..."
className="input !py-2 text-sm flex-1"
/>
<button
onClick={handleSave}
disabled={saving || !saveName.trim()}
className="btn btn-primary text-sm !py-2 disabled:opacity-50"
>
{saving ? "..." : "儲存"}
</button>
</div>
)}
{/* Prompt textarea */}
<textarea
value={promptContent}
onChange={(e) => onPromptChange(e.target.value)}
rows={12}
className="input !rounded-xl font-mono text-sm leading-relaxed resize-y"
placeholder="輸入分析提示詞...使用 {questions}, {student_answers}, {teacher_answers} 作為資料佔位符"
/>
{/* Run summary button */}
<div className="text-center">
<button
onClick={() => onRunSummary(model)}
disabled={isGenerating || !promptContent.trim()}
className="btn btn-accent text-lg px-8 py-4 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isGenerating ? (
<span className="flex items-center gap-2">
<span className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
生成報告中...
</span>
) : (
"執行摘要分析 →"
)}
</button>
</div>
</div>
);
}