Spaces:
Runtime error
Runtime error
| import { useState, useEffect } from "react"; | |
| import { ChatKit, useChatKit } from "@openai/chatkit-react"; | |
| import { CHATKIT_API_DOMAIN_KEY, CHATKIT_API_URL } from "../lib/config"; | |
| import { ReasoningPanel } from "./ReasoningPanel"; | |
| interface ExamAnalyzerProps { | |
| teacherEmail: string; | |
| onBack: () => void; | |
| } | |
| export function ExamAnalyzer({ teacherEmail, onBack }: ExamAnalyzerProps) { | |
| const [showCopiedToast, setShowCopiedToast] = useState(false); | |
| const [selectedModel, setSelectedModel] = useState("gpt-4.1-mini"); | |
| const [availableModels, setAvailableModels] = useState<Record<string, {name: string; description: string; cost: string}>>({}); | |
| // Load available models | |
| useEffect(() => { | |
| fetch("/api/models") | |
| .then(res => res.json()) | |
| .then(data => { | |
| setAvailableModels(data.models || {}); | |
| setSelectedModel(data.default || "gpt-4.1-mini"); | |
| }) | |
| .catch(err => console.error("Failed to load models:", err)); | |
| }, []); | |
| // Force Traditional Chinese locale for ChatKit | |
| useEffect(() => { | |
| // Set document language to zh-TW | |
| document.documentElement.lang = 'zh-TW'; | |
| // Try to override browser language detection | |
| if (navigator.language) { | |
| Object.defineProperty(navigator, 'language', { | |
| get: () => 'zh-TW', | |
| configurable: true, | |
| }); | |
| } | |
| // Set Accept-Language header hint (may not work for iframe) | |
| const meta = document.createElement('meta'); | |
| meta.httpEquiv = 'Content-Language'; | |
| meta.content = 'zh-TW'; | |
| document.head.appendChild(meta); | |
| }, []); | |
| // Store selected model in sessionStorage for backend access | |
| useEffect(() => { | |
| sessionStorage.setItem("selected_model", selectedModel); | |
| }, [selectedModel]); | |
| const chatkit = useChatKit({ | |
| api: { | |
| url: CHATKIT_API_URL, | |
| domainKey: CHATKIT_API_DOMAIN_KEY, | |
| }, | |
| composer: { | |
| attachments: { enabled: false }, | |
| }, | |
| }); | |
| const handleCopyPrompt = (prompt: string) => { | |
| navigator.clipboard.writeText(prompt); | |
| setShowCopiedToast(true); | |
| setTimeout(() => setShowCopiedToast(false), 2000); | |
| }; | |
| const examplePrompts = [ | |
| { | |
| title: "📊 分析 Google 試算表", | |
| prompt: `請分析這個 Google 試算表中的考試答案: | |
| https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit | |
| 標準答案是: | |
| - Q1: 4 | |
| - Q2: 加速度是速度的變化率 | |
| 我的電子郵件是 ${teacherEmail}。請評分、解釋錯誤答案、建議同儕學習小組,並將報告發送到我的電子郵件。`, | |
| }, | |
| { | |
| title: "📝 分析 CSV 資料", | |
| prompt: `請分析這些考試資料: | |
| Timestamp,Email,Student Name,Q1 (2+2),Q2 (Explain acceleration) | |
| 2026-01-26 08:00:00,alice@student.edu,Alice,4,Acceleration is the rate of change of velocity | |
| 2026-01-26 08:01:00,bob@student.edu,Bob,3,I dont know | |
| 2026-01-26 08:02:00,carol@student.edu,Carol,4,velocity change over time | |
| 2026-01-26 08:03:00,david@student.edu,David,5,speed | |
| 標準答案是: | |
| - Q1: 4 | |
| - Q2: Acceleration is the rate of change of velocity over time | |
| 我的電子郵件是 ${teacherEmail}。請評分並建立完整報告。`, | |
| }, | |
| { | |
| title: "⚡ 快速摘要", | |
| prompt: `請給我班級表現的快速摘要。我的電子郵件是 ${teacherEmail}。`, | |
| }, | |
| ]; | |
| return ( | |
| <div className="min-h-screen pt-24 pb-8 px-4"> | |
| {/* Copied toast */} | |
| {showCopiedToast && ( | |
| <div className="fixed top-24 left-1/2 -translate-x-1/2 z-50 px-4 py-2 bg-green-500 text-white rounded-lg shadow-lg"> | |
| ✓ 提示已複製!請在聊天中貼上。 | |
| </div> | |
| )} | |
| <div className="max-w-7xl mx-auto"> | |
| {/* Top bar */} | |
| <div className="flex items-center justify-between mb-6"> | |
| <button | |
| onClick={onBack} | |
| className="flex items-center gap-2 text-gray-500 hover:text-gray-700 transition-colors" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" /> | |
| </svg> | |
| 返回首頁 | |
| </button> | |
| <div className="flex items-center gap-3"> | |
| <span className="text-sm text-gray-500">已連接為</span> | |
| <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-medium"> | |
| {teacherEmail} | |
| </span> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm text-gray-500">模型:</span> | |
| <select | |
| value={selectedModel} | |
| onChange={(e) => setSelectedModel(e.target.value)} | |
| className="px-3 py-1 bg-white border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" | |
| > | |
| {Object.entries(availableModels).map(([key, model]) => ( | |
| <option key={key} value={key}> | |
| {model.name} {key === "gpt-4.1-mini" ? "(默認)" : ""} - {model.description} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="grid lg:grid-cols-4 gap-6"> | |
| {/* Left Sidebar - AI Reasoning */} | |
| <div className="lg:col-span-1 space-y-4"> | |
| {/* AI Reasoning Panel - Shows LLM thinking process */} | |
| <ReasoningPanel sessionId="default" /> | |
| {/* Example Prompts */} | |
| <div className="bg-white rounded-xl shadow-sm border p-4"> | |
| <h3 className="text-sm font-semibold text-gray-800 mb-3"> | |
| 快速開始提示 | |
| </h3> | |
| <div className="space-y-2"> | |
| {examplePrompts.map((example, i) => ( | |
| <button | |
| key={i} | |
| onClick={() => handleCopyPrompt(example.prompt)} | |
| className="w-full text-left p-3 rounded-lg bg-gray-50 hover:bg-blue-50 transition-colors group border border-transparent hover:border-blue-200" | |
| > | |
| <div className="text-sm font-medium text-gray-700 group-hover:text-blue-600"> | |
| {example.title} | |
| </div> | |
| <div className="text-xs text-gray-500"> | |
| 點擊複製 | |
| </div> | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Tips */} | |
| <div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-100"> | |
| <h3 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2"> | |
| 💡 專業提示 | |
| </h3> | |
| <ul className="space-y-1.5 text-xs text-gray-600"> | |
| <li className="flex items-start gap-1.5"> | |
| <span className="text-green-500">✓</span> | |
| 包含標準答案以提高準確度 | |
| </li> | |
| <li className="flex items-start gap-1.5"> | |
| <span className="text-green-500">✓</span> | |
| 將 Google 試算表設為公開,或使用 CSV | |
| </li> | |
| <li className="flex items-start gap-1.5"> | |
| <span className="text-green-500">✓</span> | |
| 要求以電子郵件發送報告 | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| {/* Main Chat Area - Use same structure as SimpleChatPanel */} | |
| <div className="lg:col-span-3"> | |
| <div className="bg-white rounded-xl shadow-sm border overflow-hidden"> | |
| {/* Chat Header */} | |
| <div className="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center"> | |
| <svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /> | |
| </svg> | |
| </div> | |
| <div> | |
| <h2 className="text-lg font-semibold text-white"> | |
| ClassLens 助手 | |
| </h2> | |
| <p className="text-sm text-white/70"> | |
| 請在下方貼上您的考試資料或 Google 試算表網址 | |
| </p> | |
| </div> | |
| </div> | |
| <span className="px-2 py-1 rounded-full bg-white/20 text-white text-xs"> | |
| {availableModels[selectedModel]?.name || selectedModel} | |
| </span> | |
| </div> | |
| </div> | |
| {/* ChatKit - Using the EXACT same structure as SimpleChatPanel that works */} | |
| <div className="h-[calc(100vh-320px)]"> | |
| <ChatKit | |
| control={chatkit.control} | |
| className="block h-full w-full" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |