Spaces:
Paused
Paused
| 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> | |
| ); | |
| } | |