chih.yikuan commited on
Commit
5ee5085
·
1 Parent(s): e5c2788

email-done

Browse files
.env ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ExamInsight Environment Variables
2
+ # Copy to .env for local development
3
+ # For HF Spaces, add these as Secrets in the Space settings
4
+
5
+ # =============================================================================
6
+ # REQUIRED
7
+ # =============================================================================
8
+
9
+ # OpenAI API Key (required for ChatKit)
10
+ OPENAI_API_KEY=sk-your-openai-api-key
11
+
12
+ # =============================================================================
13
+ # GOOGLE OAUTH (optional - for private Google Sheets)
14
+ # =============================================================================
15
+
16
+ # Get these from Google Cloud Console > APIs & Services > Credentials
17
+ GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
18
+ GOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret
19
+
20
+ # Redirect URI - update for production
21
+ # Local: http://localhost:8000/auth/callback
22
+ # HF Spaces: https://taboola-cz-examinsight.hf.space/auth/callback
23
+ GOOGLE_REDIRECT_URI=http://localhost:8000/auth/callback
24
+
25
+ # =============================================================================
26
+ # EMAIL (optional - for sending reports)
27
+ # =============================================================================
28
+
29
+ # Option 1: Gmail SMTP (easier setup)
30
+ GMAIL_USER=your-email@gmail.com
31
+ GMAIL_APP_PASSWORD=your-16-char-app-password
32
+
33
+ # Option 2: SendGrid API
34
+ SENDGRID_API_KEY=SG.your-sendgrid-api-key
35
+ SENDGRID_FROM_EMAIL=examinsight@yourdomain.com
36
+
37
+ # =============================================================================
38
+ # SECURITY
39
+ # =============================================================================
40
+
41
+ # Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
42
+ ENCRYPTION_KEY=your-fernet-encryption-key
43
+
44
+ # =============================================================================
45
+ # FRONTEND (for HF Spaces domain key)
46
+ # =============================================================================
47
+
48
+ # Register your HF Space domain at:
49
+ # https://platform.openai.com/settings/organization/security/domain-allowlist
50
+ VITE_CHATKIT_API_DOMAIN_KEY=domain_pk_your-production-key
chatkit/backend/app/config.py CHANGED
@@ -59,9 +59,9 @@ def get_settings() -> Settings:
59
 
60
 
61
  # Google OAuth scopes required for the application
 
62
  GOOGLE_SCOPES = [
63
  "https://www.googleapis.com/auth/spreadsheets.readonly",
64
- "https://www.googleapis.com/auth/drive.readonly",
65
  "https://www.googleapis.com/auth/userinfo.email",
66
  "openid",
67
  ]
 
59
 
60
 
61
  # Google OAuth scopes required for the application
62
+ # Note: drive.readonly is sensitive scope; spreadsheets.readonly is less restricted
63
  GOOGLE_SCOPES = [
64
  "https://www.googleapis.com/auth/spreadsheets.readonly",
 
65
  "https://www.googleapis.com/auth/userinfo.email",
66
  "openid",
67
  ]
chatkit/backend/app/main.py CHANGED
@@ -60,11 +60,51 @@ chatkit_server = ClassLensChatServer()
60
  # ChatKit Endpoint
61
  # =============================================================================
62
 
 
 
 
 
 
 
 
 
 
 
63
  @app.post("/chatkit")
64
  async def chatkit_endpoint(request: Request) -> Response:
65
  """Proxy the ChatKit web component payload to the server implementation."""
66
  payload = await request.body()
67
- result = await chatkit_server.process(payload, {"request": request})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  if isinstance(result, StreamingResult):
70
  return StreamingResponse(result, media_type="text/event-stream")
 
60
  # ChatKit Endpoint
61
  # =============================================================================
62
 
63
+ @app.get("/api/models")
64
+ async def get_available_models():
65
+ """Get list of available AI models."""
66
+ from .server import AVAILABLE_MODELS, DEFAULT_MODEL
67
+ return {
68
+ "models": AVAILABLE_MODELS,
69
+ "default": DEFAULT_MODEL,
70
+ }
71
+
72
+
73
  @app.post("/chatkit")
74
  async def chatkit_endpoint(request: Request) -> Response:
75
  """Proxy the ChatKit web component payload to the server implementation."""
76
  payload = await request.body()
77
+
78
+ # Try to extract model from multiple sources
79
+ model = (
80
+ request.headers.get("X-Model") or
81
+ request.query_params.get("model") or
82
+ # Check if model is in cookies (set by frontend)
83
+ request.cookies.get("selected_model")
84
+ )
85
+
86
+ context = {
87
+ "request": request,
88
+ "model": model, # Pass model in context
89
+ }
90
+
91
+ # Also try to extract from payload if it's JSON
92
+ try:
93
+ import json
94
+ payload_json = json.loads(payload)
95
+ if isinstance(payload_json, dict):
96
+ # Check various possible locations for model
97
+ payload_model = (
98
+ payload_json.get("metadata", {}).get("model") or
99
+ payload_json.get("model") or
100
+ payload_json.get("config", {}).get("model")
101
+ )
102
+ if payload_model:
103
+ context["model"] = payload_model
104
+ except:
105
+ pass
106
+
107
+ result = await chatkit_server.process(payload, context)
108
 
109
  if isinstance(result, StreamingResult):
110
  return StreamingResponse(result, media_type="text/event-stream")
chatkit/backend/app/oauth.py CHANGED
@@ -30,6 +30,10 @@ def get_redirect_uri(request: Request, settings) -> str:
30
  forwarded_proto = request.headers.get("x-forwarded-proto", "http")
31
  forwarded_host = request.headers.get("x-forwarded-host") or request.headers.get("host", "localhost:8000")
32
 
 
 
 
 
33
  # Build the redirect URI
34
  return f"{forwarded_proto}://{forwarded_host}/auth/callback"
35
 
@@ -61,19 +65,28 @@ async def start_auth(teacher_email: str, request: Request):
61
  _state_store[state] = {"email": teacher_email, "redirect_uri": redirect_uri}
62
 
63
  # Build authorization URL
 
 
64
  auth_params = {
65
  "client_id": settings.google_client_id,
66
  "redirect_uri": redirect_uri,
67
  "response_type": "code",
68
  "scope": " ".join(GOOGLE_SCOPES),
69
  "access_type": "offline", # Get refresh token
70
- "prompt": "consent", # Always show consent screen to get refresh token
71
  "state": state,
72
- "login_hint": teacher_email, # Pre-fill email if possible
 
73
  }
74
 
 
 
 
 
75
  print(f"[OAuth] Starting auth for {teacher_email}")
76
  print(f"[OAuth] Redirect URI: {redirect_uri}")
 
 
77
 
78
  auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(auth_params)}"
79
  return RedirectResponse(url=auth_url)
@@ -92,7 +105,7 @@ def get_frontend_url(request: Request, settings) -> str:
92
 
93
 
94
  @router.get("/callback")
95
- async def auth_callback(request: Request, code: str = None, state: str = None, error: str = None):
96
  """
97
  Handle OAuth callback from Google.
98
 
@@ -100,13 +113,20 @@ async def auth_callback(request: Request, code: str = None, state: str = None, e
100
  code: Authorization code from Google
101
  state: State token for CSRF verification
102
  error: Error message if authorization failed
 
103
  """
104
  settings = get_settings()
105
  frontend_url = get_frontend_url(request, settings)
106
 
107
  if error:
 
 
 
 
 
 
108
  return RedirectResponse(
109
- url=f"{frontend_url}?auth_error={error}"
110
  )
111
 
112
  if not code or not state:
 
30
  forwarded_proto = request.headers.get("x-forwarded-proto", "http")
31
  forwarded_host = request.headers.get("x-forwarded-host") or request.headers.get("host", "localhost:8000")
32
 
33
+ # Normalize 127.0.0.1 to localhost for local development (Google OAuth requires exact match)
34
+ if forwarded_host.startswith("127.0.0.1"):
35
+ forwarded_host = forwarded_host.replace("127.0.0.1", "localhost")
36
+
37
  # Build the redirect URI
38
  return f"{forwarded_proto}://{forwarded_host}/auth/callback"
39
 
 
65
  _state_store[state] = {"email": teacher_email, "redirect_uri": redirect_uri}
66
 
67
  # Build authorization URL
68
+ # Use "select_account consent" to allow account selection first, then show consent
69
+ # This is more flexible and works better when user isn't logged in
70
  auth_params = {
71
  "client_id": settings.google_client_id,
72
  "redirect_uri": redirect_uri,
73
  "response_type": "code",
74
  "scope": " ".join(GOOGLE_SCOPES),
75
  "access_type": "offline", # Get refresh token
76
+ "prompt": "select_account consent", # Allow account selection, then show consent
77
  "state": state,
78
+ # Only add login_hint if email is provided and valid
79
+ # This helps pre-fill but doesn't break if user isn't logged in
80
  }
81
 
82
+ # Add login_hint only if email looks valid (helps pre-fill but not required)
83
+ if teacher_email and "@" in teacher_email:
84
+ auth_params["login_hint"] = teacher_email
85
+
86
  print(f"[OAuth] Starting auth for {teacher_email}")
87
  print(f"[OAuth] Redirect URI: {redirect_uri}")
88
+ print(f"[OAuth] Scopes: {GOOGLE_SCOPES}")
89
+ print(f"[OAuth] Full auth URL: https://accounts.google.com/o/oauth2/v2/auth?{urlencode(auth_params)}")
90
 
91
  auth_url = f"https://accounts.google.com/o/oauth2/v2/auth?{urlencode(auth_params)}"
92
  return RedirectResponse(url=auth_url)
 
105
 
106
 
107
  @router.get("/callback")
108
+ async def auth_callback(request: Request, code: str = None, state: str = None, error: str = None, error_description: str = None):
109
  """
110
  Handle OAuth callback from Google.
111
 
 
113
  code: Authorization code from Google
114
  state: State token for CSRF verification
115
  error: Error message if authorization failed
116
+ error_description: Detailed error description from Google
117
  """
118
  settings = get_settings()
119
  frontend_url = get_frontend_url(request, settings)
120
 
121
  if error:
122
+ # Log the full error details
123
+ error_msg = error
124
+ if error_description:
125
+ error_msg = f"{error}: {error_description}"
126
+ print(f"[OAuth] Error callback: {error_msg}")
127
+ print(f"[OAuth] Full query params: {dict(request.query_params)}")
128
  return RedirectResponse(
129
+ url=f"{frontend_url}?auth_error={error_msg}"
130
  )
131
 
132
  if not code or not state:
chatkit/backend/app/server.py CHANGED
@@ -18,7 +18,46 @@ from .status_tracker import update_status, add_reasoning_step, WorkflowStep, res
18
 
19
 
20
  MAX_RECENT_ITEMS = 50
21
- MODEL = "gpt-4.1-mini" # Using mini for cost efficiency (~10x cheaper)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  # Current session ID (simplified - in production use proper session management)
24
  _current_session_id = "default"
@@ -29,6 +68,39 @@ def set_session_id(session_id: str):
29
  _current_session_id = session_id
30
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  # =============================================================================
33
  # Tool Definitions with Status Tracking
34
  # =============================================================================
@@ -199,6 +271,12 @@ async def log_reasoning(thought: str) -> str:
199
 
200
  EXAMINSIGHT_INSTRUCTIONS = """You are ClassLens, an AI teaching assistant that creates beautiful, comprehensive exam analysis reports for teachers.
201
 
 
 
 
 
 
 
202
  ## Your Core Mission
203
 
204
  Transform raw Google Form quiz responses into professional, teacher-ready HTML reports with:
@@ -343,15 +421,62 @@ new Chart(document.getElementById('questionChart').getContext('2d'), {
343
  });
344
  ```
345
 
346
- ### Step 5: Send Report
347
- Use `send_report_email` with the complete HTML as the body.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
 
349
  ## Output Format
350
 
351
- When generating the report:
352
- 1. First display a brief summary in chat
353
- 2. Then output the complete HTML in a code block
354
- 3. Offer to email it to the teacher
 
 
355
 
356
  ## Design Principles
357
 
@@ -375,24 +500,40 @@ When generating the report:
375
  - Never expose full student identifiers
376
  - Group low performers sensitively
377
 
378
- Start by greeting the teacher and asking for:
379
- 1. Google Form/Sheet URL
380
- 2. Their email (for sending the report)
381
- 3. Optionally: correct answers if not embedded in the form"""
382
 
 
383
 
384
- classlens_agent = Agent[AgentContext[dict[str, Any]]](
385
- model=MODEL,
386
- name="ClassLens",
387
- instructions=EXAMINSIGHT_INSTRUCTIONS,
388
- tools=[
389
- fetch_responses,
390
- parse_csv_data,
391
- send_report_email,
392
- save_analysis_report,
393
- log_reasoning,
394
- ],
395
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
 
398
  # =============================================================================
@@ -416,6 +557,12 @@ class ClassLensChatServer(ChatKitServer[dict[str, Any]]):
416
  reset_status(thread.id)
417
  set_session_id(thread.id)
418
 
 
 
 
 
 
 
419
  items_page = await self.store.load_thread_items(
420
  thread.id,
421
  after=None,
@@ -433,7 +580,7 @@ class ClassLensChatServer(ChatKitServer[dict[str, Any]]):
433
  )
434
 
435
  result = Runner.run_streamed(
436
- classlens_agent,
437
  agent_input,
438
  context=agent_context,
439
  )
 
18
 
19
 
20
  MAX_RECENT_ITEMS = 50
21
+ DEFAULT_MODEL = "gpt-4.1-mini" # Default model for cost efficiency (~10x cheaper)
22
+
23
+ # Available models
24
+ AVAILABLE_MODELS = {
25
+ "gpt-4.1-mini": {
26
+ "name": "GPT-4.1-mini",
27
+ "description": "快速且經濟實惠(默認)",
28
+ "cost": "低",
29
+ },
30
+ "gpt-4o": {
31
+ "name": "GPT-4o",
32
+ "description": "高性能,適合複雜分析",
33
+ "cost": "中",
34
+ },
35
+ "gpt-4o-mini": {
36
+ "name": "GPT-4o-mini",
37
+ "description": "平衡性能與成本",
38
+ "cost": "低",
39
+ },
40
+ "gpt-5-pro": {
41
+ "name": "GPT-5 Pro",
42
+ "description": "最新旗艦模型,具備強大推理能力",
43
+ "cost": "高",
44
+ },
45
+ "o3-pro": {
46
+ "name": "O3 Pro",
47
+ "description": "深度推理模型,適合複雜問題分析",
48
+ "cost": "高",
49
+ },
50
+ "o1-preview": {
51
+ "name": "O1 Preview",
52
+ "description": "具備深度推理能力",
53
+ "cost": "高",
54
+ },
55
+ "o1-mini": {
56
+ "name": "O1 Mini",
57
+ "description": "具備推理能力,經濟實惠",
58
+ "cost": "中",
59
+ },
60
+ }
61
 
62
  # Current session ID (simplified - in production use proper session management)
63
  _current_session_id = "default"
 
68
  _current_session_id = session_id
69
 
70
 
71
+ def get_model_from_context(context: dict[str, Any]) -> str:
72
+ """Get model from context, default to DEFAULT_MODEL if not specified."""
73
+ # Try multiple ways to get model from context
74
+ model = (
75
+ context.get("model") or
76
+ context.get("request", {}).get("model") or
77
+ # Check query params
78
+ (context.get("request", {}).get("query_params", {}).get("model") if hasattr(context.get("request", {}), "query_params") else None) or
79
+ # Check headers
80
+ (context.get("request", {}).headers.get("x-model") if hasattr(context.get("request", {}), "headers") else None)
81
+ )
82
+
83
+ if model and model in AVAILABLE_MODELS:
84
+ return model
85
+ return DEFAULT_MODEL
86
+
87
+
88
+ def create_agent(model: str) -> Agent[AgentContext[dict[str, Any]]]:
89
+ """Create an agent with the specified model."""
90
+ return Agent[AgentContext[dict[str, Any]]](
91
+ model=model,
92
+ name="ClassLens",
93
+ instructions=EXAMINSIGHT_INSTRUCTIONS,
94
+ tools=[
95
+ fetch_responses,
96
+ parse_csv_data,
97
+ send_report_email,
98
+ save_analysis_report,
99
+ log_reasoning,
100
+ ],
101
+ )
102
+
103
+
104
  # =============================================================================
105
  # Tool Definitions with Status Tracking
106
  # =============================================================================
 
271
 
272
  EXAMINSIGHT_INSTRUCTIONS = """You are ClassLens, an AI teaching assistant that creates beautiful, comprehensive exam analysis reports for teachers.
273
 
274
+ ## Language & Communication
275
+ - Always communicate in Traditional Chinese (繁體中文, zh-TW)
276
+ - Use polite and professional language
277
+ - When greeting users, say: "您好!我是 ClassLens 助手,今天能為您做些什麼?"
278
+ - All responses, explanations, and reports should be in Traditional Chinese unless the user specifically requests English
279
+
280
  ## Your Core Mission
281
 
282
  Transform raw Google Form quiz responses into professional, teacher-ready HTML reports with:
 
421
  });
422
  ```
423
 
424
+ ### Step 5: Summary and Email Report
425
+
426
+ **CRITICAL: After analyzing the exam data, follow these steps:**
427
+
428
+ 1. **Display a bullet-point summary** in chat (in Traditional Chinese):
429
+ - 總學生數、平均分數、最高分、最低分
430
+ - 各題正確率(例如:Q1: 85%, Q2: 60%, Q3: 90%)
431
+ - 主要發現(例如:多數學生在 Q2 答錯,可能對某概念理解不足)
432
+ - 需要關注的學生(低分學生)
433
+ - 表現優秀的學生(可作為同儕導師)
434
+
435
+ Format example:
436
+ ```
437
+ 📊 考試分析摘要:
438
+
439
+ • 總學生數:25 人
440
+ • 平均分數:72 分(滿分 100)
441
+ • 最高分:95 分,最低分:45 分
442
+
443
+ 📈 各題正確率:
444
+ • Q1:85% 正確
445
+ • Q2:60% 正確 ⚠️(需要加強)
446
+ • Q3:90% 正確
447
+
448
+ 🔍 主要發現:
449
+ • 多數學生在 Q2 答錯,可能對「加速度」概念理解不足
450
+ • 約 30% 學生在開放式問題中表達不清楚
451
+
452
+ 👥 需要關注的學生:3 人(分數低於 50 分)
453
+ ⭐ 表現優秀的學生:5 人(分數高於 90 分,可作為同儕導師)
454
+ ```
455
+
456
+ 2. **Ask for confirmation**: After showing the summary, ask in Traditional Chinese:
457
+ "以上是分析摘要。您希望我生成完整的 HTML 詳細��告並發送到您的電子郵件嗎?"
458
+
459
+ 3. **Wait for user confirmation** before generating and sending the HTML report
460
+ - Only proceed when the teacher explicitly confirms (says "yes", "send", "發送", "好的", "是", "要", "生成", "寄給我", etc.)
461
+
462
+ 4. **After confirmation**:
463
+ - Generate the complete HTML report with all sections (Q&A Analysis, Statistics with charts, Teacher Insights)
464
+ - Create an appropriate email subject line (e.g., "考試分析報告 - [Quiz Title] - [Date]" or "ClassLens 考試分析報告")
465
+ - Call `send_report_email` with:
466
+ * email: teacher's email address
467
+ * subject: the email subject line you created
468
+ * body_markdown: the complete HTML report content
469
+ - After successfully sending, confirm with: "報告已生成並發送到您的電子郵件「[subject]」。報告包含詳細的題目分析、統計圖表和教學建議。請檢查您的收件匣。"
470
+ - Make sure to include the actual subject line in the confirmation message (replace [subject] with the actual subject you used)
471
 
472
  ## Output Format
473
 
474
+ When analyzing exam data:
475
+ 1. First display a bullet-point summary in chat (in Traditional Chinese)
476
+ 2. Ask: "以上是分析摘要。您希望我生成完整的 HTML 詳細報告並發送到您的電子郵件嗎?"
477
+ 3. Wait for user confirmation
478
+ 4. Only after confirmation: Generate complete HTML report and send via email
479
+ 5. Do NOT automatically generate or send the HTML report - always show summary first and ask for confirmation
480
 
481
  ## Design Principles
482
 
 
500
  - Never expose full student identifiers
501
  - Group low performers sensitively
502
 
503
+ ## Initial Conversation Flow
 
 
 
504
 
505
+ When starting a new conversation, follow this sequence:
506
 
507
+ 1. **Greet the teacher** in Traditional Chinese: "您好!我是 ClassLens 助手,今天能為您做些什麼?"
508
+
509
+ 2. **Ask for Google Form/Sheet URL**: "請提供您的 Google 表單或試算表網址。"
510
+
511
+ 3. **Ask about answer key** (標準答案):
512
+ - "請問您是否方便提供本次考試的正確答案(標準答案)?"
513
+ - "如果您有標準答案,請提供給我,這樣我可以更準確地評分和分析。"
514
+ - "如果您沒有標準答案,我可以嘗試根據學生的回答模式自動推斷標準答案。您希望我為您自動生成標準答案嗎?"
515
+
516
+ 4. **Ask for email**: "請提供您的電子郵件地址,以便我將分析報告發送給您。"
517
+
518
+ 5. **Wait for all information** before starting analysis:
519
+ - Google Form/Sheet URL (required)
520
+ - Answer key (optional, but recommended for accuracy)
521
+ - Teacher email (required for sending report)
522
+
523
+ ## Answer Key Handling
524
+
525
+ - **If teacher provides answer key**: Use it directly for accurate grading
526
+ - **If teacher doesn't have answer key but wants auto-generation**:
527
+ - Analyze student responses to infer the most likely correct answers
528
+ - Show the inferred answers to the teacher for confirmation
529
+ - Ask: "根據學生回答模式,我推斷的標準答案如下:[列出答案]。請確認這些答案是否正確,或提供修正。"
530
+ - **If teacher doesn't provide and doesn't want auto-generation**:
531
+ - Proceed with analysis but note that grading accuracy may be limited
532
+ - Focus on response patterns and common mistakes rather than absolute correctness"""
533
+
534
+
535
+ # Default agent (will be overridden by dynamic agent creation in respond method)
536
+ classlens_agent = create_agent(DEFAULT_MODEL)
537
 
538
 
539
  # =============================================================================
 
557
  reset_status(thread.id)
558
  set_session_id(thread.id)
559
 
560
+ # Get model from context (user selection from frontend)
561
+ selected_model = get_model_from_context(context)
562
+
563
+ # Create agent with selected model
564
+ agent = create_agent(selected_model)
565
+
566
  items_page = await self.store.load_thread_items(
567
  thread.id,
568
  after=None,
 
580
  )
581
 
582
  result = Runner.run_streamed(
583
+ agent,
584
  agent_input,
585
  context=agent_context,
586
  )
chatkit/frontend/index.html CHANGED
@@ -1,5 +1,5 @@
1
  <!doctype html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
1
  <!doctype html>
2
+ <html lang="zh-TW">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
chatkit/frontend/src/App.tsx CHANGED
@@ -50,7 +50,7 @@ function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
50
  ClassLens
51
  </h1>
52
  <p className="text-[var(--color-text-muted)] mt-2">
53
- AI-Powered Exam Analysis
54
  </p>
55
  </div>
56
 
@@ -61,7 +61,7 @@ function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
61
  htmlFor="invite-code"
62
  className="block text-sm font-medium text-[var(--color-text-muted)] mb-2"
63
  >
64
- Enter Invite Code
65
  </label>
66
  <input
67
  id="invite-code"
@@ -71,7 +71,7 @@ function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
71
  setCode(e.target.value);
72
  setError(false);
73
  }}
74
- placeholder="Enter your invite code..."
75
  className={`w-full px-4 py-3 bg-[var(--color-background)] border rounded-xl text-[var(--color-text)] placeholder-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-all ${
76
  error
77
  ? "border-red-500 focus:ring-red-500"
@@ -84,7 +84,7 @@ function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
84
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
86
  </svg>
87
- Invalid invite code. Please try again.
88
  </p>
89
  )}
90
  </div>
@@ -93,7 +93,7 @@ function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) {
93
  type="submit"
94
  className="w-full py-3 px-4 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-accent)] text-white font-semibold rounded-xl hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface)]"
95
  >
96
- Access App
97
  </button>
98
  </form>
99
 
 
50
  ClassLens
51
  </h1>
52
  <p className="text-[var(--color-text-muted)] mt-2">
53
+ AI 驅動的考試分析
54
  </p>
55
  </div>
56
 
 
61
  htmlFor="invite-code"
62
  className="block text-sm font-medium text-[var(--color-text-muted)] mb-2"
63
  >
64
+ 輸入邀請碼
65
  </label>
66
  <input
67
  id="invite-code"
 
71
  setCode(e.target.value);
72
  setError(false);
73
  }}
74
+ placeholder="請輸入您的邀請碼..."
75
  className={`w-full px-4 py-3 bg-[var(--color-background)] border rounded-xl text-[var(--color-text)] placeholder-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-all ${
76
  error
77
  ? "border-red-500 focus:ring-red-500"
 
84
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
86
  </svg>
87
+ 邀請碼無效,請重試。
88
  </p>
89
  )}
90
  </div>
 
93
  type="submit"
94
  className="w-full py-3 px-4 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-accent)] text-white font-semibold rounded-xl hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface)]"
95
  >
96
+ 進入應用程式
97
  </button>
98
  </form>
99
 
chatkit/frontend/src/components/AuthStatus.tsx CHANGED
@@ -45,7 +45,7 @@ export function AuthStatus({
45
  setError(null);
46
 
47
  if (!emailInput.trim()) {
48
- setError("Please enter your email address");
49
  return;
50
  }
51
 
@@ -69,7 +69,7 @@ export function AuthStatus({
69
  const data = await response.json();
70
  if (data.detail?.includes('not configured')) {
71
  setAuthMode('csv-only');
72
- setError("Google OAuth not configured. You can still use the app by uploading CSV files directly in the chat!");
73
  // Set as "connected" with just email for CSV fallback
74
  setTeacherEmail(emailInput);
75
  setIsAuthenticated(true);
@@ -110,12 +110,12 @@ export function AuthStatus({
110
  </svg>
111
  </div>
112
  <h2 className="font-display text-2xl font-bold text-[var(--color-text)] mb-2">
113
- {isAuthenticated ? "You're Connected!" : "Connect Your Google Account"}
114
  </h2>
115
  <p className="text-[var(--color-text-muted)]">
116
  {isAuthenticated
117
- ? "Your Google account is linked. You can now analyze exam responses."
118
- : "We'll only access your Google Forms response spreadsheets. No data is stored on our servers."}
119
  </p>
120
  </div>
121
 
@@ -130,14 +130,14 @@ export function AuthStatus({
130
  <div className="text-left">
131
  <div className="font-medium text-[var(--color-text)]">{teacherEmail}</div>
132
  <div className="text-sm text-[var(--color-text-muted)]">
133
- {authMode === 'csv-only' ? 'CSV Upload Mode (Google OAuth not configured)' : 'Google Account Connected'}
134
  </div>
135
  </div>
136
  </div>
137
 
138
  {authMode === 'csv-only' && (
139
  <div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-sm">
140
- <strong>Note:</strong> You can paste CSV data directly in the chat, or set up Google OAuth to fetch from Google Forms.
141
  </div>
142
  )}
143
 
@@ -145,14 +145,14 @@ export function AuthStatus({
145
  onClick={handleDisconnect}
146
  className="w-full btn btn-outline text-red-500 border-red-200 hover:border-red-500 hover:text-red-600"
147
  >
148
- Disconnect Account
149
  </button>
150
  </div>
151
  ) : (
152
  <form onSubmit={handleConnect} className="space-y-4">
153
  <div>
154
  <label htmlFor="email" className="block text-sm font-medium text-[var(--color-text)] mb-2">
155
- Your School Email
156
  </label>
157
  <input
158
  type="email"
@@ -182,7 +182,7 @@ export function AuthStatus({
182
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
183
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
184
  </svg>
185
- Checking...
186
  </>
187
  ) : (
188
  <>
@@ -192,16 +192,16 @@ export function AuthStatus({
192
  <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
193
  <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
194
  </svg>
195
- Continue with Google
196
  </>
197
  )}
198
  </button>
199
 
200
  <p className="text-center text-xs text-[var(--color-text-muted)]">
201
- By connecting, you agree to our{" "}
202
- <a href="#" className="underline hover:text-[var(--color-primary)]">Terms of Service</a>
203
- {" "}and{" "}
204
- <a href="#" className="underline hover:text-[var(--color-primary)]">Privacy Policy</a>
205
  </p>
206
  </form>
207
  )}
 
45
  setError(null);
46
 
47
  if (!emailInput.trim()) {
48
+ setError("請輸入您的電子郵件地址");
49
  return;
50
  }
51
 
 
69
  const data = await response.json();
70
  if (data.detail?.includes('not configured')) {
71
  setAuthMode('csv-only');
72
+ setError("Google OAuth 未設定。您仍可在聊天中直接上傳 CSV 檔案使用應用程式!");
73
  // Set as "connected" with just email for CSV fallback
74
  setTeacherEmail(emailInput);
75
  setIsAuthenticated(true);
 
110
  </svg>
111
  </div>
112
  <h2 className="font-display text-2xl font-bold text-[var(--color-text)] mb-2">
113
+ {isAuthenticated ? "已連接!" : "連接您的 Google 帳號"}
114
  </h2>
115
  <p className="text-[var(--color-text-muted)]">
116
  {isAuthenticated
117
+ ? "您的 Google 帳號已連結。現在可以分析考試答案了。"
118
+ : "我們只會存取您的 Google 表單回應試算表。不會在我們的伺服器上儲存任何資料。"}
119
  </p>
120
  </div>
121
 
 
130
  <div className="text-left">
131
  <div className="font-medium text-[var(--color-text)]">{teacherEmail}</div>
132
  <div className="text-sm text-[var(--color-text-muted)]">
133
+ {authMode === 'csv-only' ? 'CSV 上傳模式(Google OAuth 未設定)' : 'Google 帳號已連接'}
134
  </div>
135
  </div>
136
  </div>
137
 
138
  {authMode === 'csv-only' && (
139
  <div className="p-3 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-sm">
140
+ <strong>注意:</strong>您可以在聊天中直接貼上 CSV 資料,或設定 Google OAuth Google 表單取得資料。
141
  </div>
142
  )}
143
 
 
145
  onClick={handleDisconnect}
146
  className="w-full btn btn-outline text-red-500 border-red-200 hover:border-red-500 hover:text-red-600"
147
  >
148
+ 斷開帳號
149
  </button>
150
  </div>
151
  ) : (
152
  <form onSubmit={handleConnect} className="space-y-4">
153
  <div>
154
  <label htmlFor="email" className="block text-sm font-medium text-[var(--color-text)] mb-2">
155
+ 您的學校電子郵件
156
  </label>
157
  <input
158
  type="email"
 
182
  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
183
  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
184
  </svg>
185
+ 檢查中...
186
  </>
187
  ) : (
188
  <>
 
192
  <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
193
  <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
194
  </svg>
195
+ 使用 Google 繼續
196
  </>
197
  )}
198
  </button>
199
 
200
  <p className="text-center text-xs text-[var(--color-text-muted)]">
201
+ 連接即表示您同意我們的{" "}
202
+ <a href="#" className="underline hover:text-[var(--color-primary)]">服務條款</a>
203
+ {" "}{" "}
204
+ <a href="#" className="underline hover:text-[var(--color-primary)]">隱私政策</a>
205
  </p>
206
  </form>
207
  )}
chatkit/frontend/src/components/ExamAnalyzer.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
2
  import { ChatKit, useChatKit } from "@openai/chatkit-react";
3
  import { CHATKIT_API_DOMAIN_KEY, CHATKIT_API_URL } from "../lib/config";
4
  import { ReasoningPanel } from "./ReasoningPanel";
@@ -10,9 +10,50 @@ interface ExamAnalyzerProps {
10
 
11
  export function ExamAnalyzer({ teacherEmail, onBack }: ExamAnalyzerProps) {
12
  const [showCopiedToast, setShowCopiedToast] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  const chatkit = useChatKit({
15
- api: { url: CHATKIT_API_URL, domainKey: CHATKIT_API_DOMAIN_KEY },
 
 
 
16
  composer: {
17
  attachments: { enabled: false },
18
  },
@@ -26,19 +67,19 @@ export function ExamAnalyzer({ teacherEmail, onBack }: ExamAnalyzerProps) {
26
 
27
  const examplePrompts = [
28
  {
29
- title: "📊 Analyze Google Sheet",
30
- prompt: `Please analyze the exam responses from this Google Sheet:
31
  https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit
32
 
33
- The correct answers are:
34
  - Q1: 4
35
- - Q2: Acceleration is the rate of change of velocity
36
 
37
- My email is ${teacherEmail}. Please grade the responses, explain incorrect answers, suggest peer learning groups, and email me the report.`,
38
  },
39
  {
40
- title: "📝 Analyze CSV Data",
41
- prompt: `Please analyze this exam data:
42
 
43
  Timestamp,Email,Student Name,Q1 (2+2),Q2 (Explain acceleration)
44
  2026-01-26 08:00:00,alice@student.edu,Alice,4,Acceleration is the rate of change of velocity
@@ -46,15 +87,15 @@ Timestamp,Email,Student Name,Q1 (2+2),Q2 (Explain acceleration)
46
  2026-01-26 08:02:00,carol@student.edu,Carol,4,velocity change over time
47
  2026-01-26 08:03:00,david@student.edu,David,5,speed
48
 
49
- The correct answers are:
50
  - Q1: 4
51
  - Q2: Acceleration is the rate of change of velocity over time
52
 
53
- My email is ${teacherEmail}. Please grade and create a full report.`,
54
  },
55
  {
56
- title: "⚡ Quick Summary",
57
- prompt: `Just give me a quick summary of class performance. My email is ${teacherEmail}.`,
58
  },
59
  ];
60
 
@@ -63,7 +104,7 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
63
  {/* Copied toast */}
64
  {showCopiedToast && (
65
  <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">
66
- Prompt copied! Paste it in the chat.
67
  </div>
68
  )}
69
 
@@ -77,14 +118,28 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
77
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
79
  </svg>
80
- Back to Home
81
  </button>
82
 
83
  <div className="flex items-center gap-3">
84
- <span className="text-sm text-gray-500">Connected as</span>
85
  <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-medium">
86
  {teacherEmail}
87
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
  </div>
90
 
@@ -97,7 +152,7 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
97
  {/* Example Prompts */}
98
  <div className="bg-white rounded-xl shadow-sm border p-4">
99
  <h3 className="text-sm font-semibold text-gray-800 mb-3">
100
- Quick Start Prompts
101
  </h3>
102
  <div className="space-y-2">
103
  {examplePrompts.map((example, i) => (
@@ -110,7 +165,7 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
110
  {example.title}
111
  </div>
112
  <div className="text-xs text-gray-500">
113
- Click to copy
114
  </div>
115
  </button>
116
  ))}
@@ -120,20 +175,20 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
120
  {/* Tips */}
121
  <div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-100">
122
  <h3 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2">
123
- 💡 Pro Tips
124
  </h3>
125
  <ul className="space-y-1.5 text-xs text-gray-600">
126
  <li className="flex items-start gap-1.5">
127
  <span className="text-green-500">✓</span>
128
- Include answer key for accuracy
129
  </li>
130
  <li className="flex items-start gap-1.5">
131
  <span className="text-green-500">✓</span>
132
- Make Google Sheet public, or use CSV
133
  </li>
134
  <li className="flex items-start gap-1.5">
135
  <span className="text-green-500">✓</span>
136
- Ask for email delivery
137
  </li>
138
  </ul>
139
  </div>
@@ -153,15 +208,15 @@ My email is ${teacherEmail}. Please grade and create a full report.`,
153
  </div>
154
  <div>
155
  <h2 className="text-lg font-semibold text-white">
156
- ClassLens Assistant
157
  </h2>
158
  <p className="text-sm text-white/70">
159
- Paste your exam data or Google Sheet URL below
160
  </p>
161
  </div>
162
  </div>
163
  <span className="px-2 py-1 rounded-full bg-white/20 text-white text-xs">
164
- GPT-4.1-mini
165
  </span>
166
  </div>
167
  </div>
 
1
+ import { useState, useEffect } from "react";
2
  import { ChatKit, useChatKit } from "@openai/chatkit-react";
3
  import { CHATKIT_API_DOMAIN_KEY, CHATKIT_API_URL } from "../lib/config";
4
  import { ReasoningPanel } from "./ReasoningPanel";
 
10
 
11
  export function ExamAnalyzer({ teacherEmail, onBack }: ExamAnalyzerProps) {
12
  const [showCopiedToast, setShowCopiedToast] = useState(false);
13
+ const [selectedModel, setSelectedModel] = useState("gpt-4.1-mini");
14
+ const [availableModels, setAvailableModels] = useState<Record<string, {name: string; description: string; cost: string}>>({});
15
+
16
+ // Load available models
17
+ useEffect(() => {
18
+ fetch("/api/models")
19
+ .then(res => res.json())
20
+ .then(data => {
21
+ setAvailableModels(data.models || {});
22
+ setSelectedModel(data.default || "gpt-4.1-mini");
23
+ })
24
+ .catch(err => console.error("Failed to load models:", err));
25
+ }, []);
26
+
27
+ // Force Traditional Chinese locale for ChatKit
28
+ useEffect(() => {
29
+ // Set document language to zh-TW
30
+ document.documentElement.lang = 'zh-TW';
31
+
32
+ // Try to override browser language detection
33
+ if (navigator.language) {
34
+ Object.defineProperty(navigator, 'language', {
35
+ get: () => 'zh-TW',
36
+ configurable: true,
37
+ });
38
+ }
39
+
40
+ // Set Accept-Language header hint (may not work for iframe)
41
+ const meta = document.createElement('meta');
42
+ meta.httpEquiv = 'Content-Language';
43
+ meta.content = 'zh-TW';
44
+ document.head.appendChild(meta);
45
+ }, []);
46
+
47
+ // Store selected model in sessionStorage for backend access
48
+ useEffect(() => {
49
+ sessionStorage.setItem("selected_model", selectedModel);
50
+ }, [selectedModel]);
51
 
52
  const chatkit = useChatKit({
53
+ api: {
54
+ url: CHATKIT_API_URL,
55
+ domainKey: CHATKIT_API_DOMAIN_KEY,
56
+ },
57
  composer: {
58
  attachments: { enabled: false },
59
  },
 
67
 
68
  const examplePrompts = [
69
  {
70
+ title: "📊 分析 Google 試算表",
71
+ prompt: `請分析這個 Google 試算表中的考試答案:
72
  https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit
73
 
74
+ 標準答案是:
75
  - Q1: 4
76
+ - Q2: 加速度是速度的變化率
77
 
78
+ 我的電子郵件是 ${teacherEmail}。請評分、解釋錯誤答案、建議同儕學習小組,並將報告發送到我的電子郵件。`,
79
  },
80
  {
81
+ title: "📝 分析 CSV 資料",
82
+ prompt: `請分析這些考試資料:
83
 
84
  Timestamp,Email,Student Name,Q1 (2+2),Q2 (Explain acceleration)
85
  2026-01-26 08:00:00,alice@student.edu,Alice,4,Acceleration is the rate of change of velocity
 
87
  2026-01-26 08:02:00,carol@student.edu,Carol,4,velocity change over time
88
  2026-01-26 08:03:00,david@student.edu,David,5,speed
89
 
90
+ 標準答案是:
91
  - Q1: 4
92
  - Q2: Acceleration is the rate of change of velocity over time
93
 
94
+ 我的電子郵件是 ${teacherEmail}。請評分並建立完整報告。`,
95
  },
96
  {
97
+ title: "⚡ 快速摘要",
98
+ prompt: `請給我班級表現的快速摘要。我的電子郵件是 ${teacherEmail}`,
99
  },
100
  ];
101
 
 
104
  {/* Copied toast */}
105
  {showCopiedToast && (
106
  <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">
107
+ 提示已複製!請在聊天中貼上。
108
  </div>
109
  )}
110
 
 
118
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
119
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
120
  </svg>
121
+ 返回首頁
122
  </button>
123
 
124
  <div className="flex items-center gap-3">
125
+ <span className="text-sm text-gray-500">已連接為</span>
126
  <span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-medium">
127
  {teacherEmail}
128
  </span>
129
+ <div className="flex items-center gap-2">
130
+ <span className="text-sm text-gray-500">模型:</span>
131
+ <select
132
+ value={selectedModel}
133
+ onChange={(e) => setSelectedModel(e.target.value)}
134
+ 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"
135
+ >
136
+ {Object.entries(availableModels).map(([key, model]) => (
137
+ <option key={key} value={key}>
138
+ {model.name} {key === "gpt-4.1-mini" ? "(默認)" : ""} - {model.description}
139
+ </option>
140
+ ))}
141
+ </select>
142
+ </div>
143
  </div>
144
  </div>
145
 
 
152
  {/* Example Prompts */}
153
  <div className="bg-white rounded-xl shadow-sm border p-4">
154
  <h3 className="text-sm font-semibold text-gray-800 mb-3">
155
+ 快速開始提示
156
  </h3>
157
  <div className="space-y-2">
158
  {examplePrompts.map((example, i) => (
 
165
  {example.title}
166
  </div>
167
  <div className="text-xs text-gray-500">
168
+ 點擊複製
169
  </div>
170
  </button>
171
  ))}
 
175
  {/* Tips */}
176
  <div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-100">
177
  <h3 className="text-sm font-semibold text-gray-800 mb-2 flex items-center gap-2">
178
+ 💡 專業提示
179
  </h3>
180
  <ul className="space-y-1.5 text-xs text-gray-600">
181
  <li className="flex items-start gap-1.5">
182
  <span className="text-green-500">✓</span>
183
+ 包含標準答案以提高準確度
184
  </li>
185
  <li className="flex items-start gap-1.5">
186
  <span className="text-green-500">✓</span>
187
+ Google 試算表設為公開,或使用 CSV
188
  </li>
189
  <li className="flex items-start gap-1.5">
190
  <span className="text-green-500">✓</span>
191
+ 要求以電子郵件發送報告
192
  </li>
193
  </ul>
194
  </div>
 
208
  </div>
209
  <div>
210
  <h2 className="text-lg font-semibold text-white">
211
+ ClassLens 助手
212
  </h2>
213
  <p className="text-sm text-white/70">
214
+ 請在下方貼上您的考試資料或 Google 試算表網址
215
  </p>
216
  </div>
217
  </div>
218
  <span className="px-2 py-1 rounded-full bg-white/20 text-white text-xs">
219
+ {availableModels[selectedModel]?.name || selectedModel}
220
  </span>
221
  </div>
222
  </div>
chatkit/frontend/src/components/Features.tsx CHANGED
@@ -6,8 +6,8 @@ export function Features() {
6
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
7
  </svg>
8
  ),
9
- title: "Automatic Grading",
10
- description: "Grade multiple choice, numeric, and even open-ended questions with AI-powered accuracy.",
11
  color: "var(--color-primary)",
12
  },
13
  {
@@ -16,8 +16,8 @@ export function Features() {
16
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
17
  </svg>
18
  ),
19
- title: "Smart Explanations",
20
- description: "Get personalized explanations for each wrong answer, helping students understand their mistakes.",
21
  color: "var(--color-accent)",
22
  },
23
  {
@@ -26,8 +26,8 @@ export function Features() {
26
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
27
  </svg>
28
  ),
29
- title: "Peer Learning Groups",
30
- description: "AI-suggested student groupings pair high performers with those who need help on specific topics.",
31
  color: "var(--color-success)",
32
  },
33
  {
@@ -36,8 +36,8 @@ export function Features() {
36
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
37
  </svg>
38
  ),
39
- title: "Email Reports",
40
- description: "Receive beautiful, formatted reports directly in your inbox, ready to share with students or parents.",
41
  color: "var(--color-warning)",
42
  },
43
  {
@@ -46,8 +46,8 @@ export function Features() {
46
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
47
  </svg>
48
  ),
49
- title: "Privacy First",
50
- description: "Student data is processed securely. We only access what you share and never store sensitive information.",
51
  color: "var(--color-primary-light)",
52
  },
53
  {
@@ -56,8 +56,8 @@ export function Features() {
56
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
57
  </svg>
58
  ),
59
- title: "Instant Analysis",
60
- description: "Get comprehensive insights in seconds, not hours. More time teaching, less time grading.",
61
  color: "var(--color-accent-light)",
62
  },
63
  ];
@@ -67,12 +67,11 @@ export function Features() {
67
  <div className="max-w-6xl mx-auto">
68
  <div className="text-center mb-16">
69
  <h2 className="font-display text-4xl font-bold text-[var(--color-text)] mb-4">
70
- Everything You Need to
71
- <span className="text-gradient"> Understand Your Students</span>
72
  </h2>
73
  <p className="text-lg text-[var(--color-text-muted)] max-w-2xl mx-auto">
74
- ClassLens combines the power of AI with thoughtful education practices
75
- to give you actionable insights.
76
  </p>
77
  </div>
78
 
 
6
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
7
  </svg>
8
  ),
9
+ title: "自動評分",
10
+ description: "使用 AI 精準評分選擇題、數值題,甚至開放式問題。",
11
  color: "var(--color-primary)",
12
  },
13
  {
 
16
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
17
  </svg>
18
  ),
19
+ title: "智能解釋",
20
+ description: "為每個錯誤答案提供個人化解釋,幫助學生理解錯誤。",
21
  color: "var(--color-accent)",
22
  },
23
  {
 
26
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
27
  </svg>
28
  ),
29
+ title: "同儕學習小組",
30
+ description: "AI 建議的學生分組,將表現優秀的學生與需要特定主題幫助的學生配對。",
31
  color: "var(--color-success)",
32
  },
33
  {
 
36
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
37
  </svg>
38
  ),
39
+ title: "電子郵件報告",
40
+ description: "直接在收件匣中接收精美的格式化報告,隨時可與學生或家長分享。",
41
  color: "var(--color-warning)",
42
  },
43
  {
 
46
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
47
  </svg>
48
  ),
49
+ title: "隱私優先",
50
+ description: "學生資料安全處理。我們只存取您分享的內容,絕不儲存敏感資訊。",
51
  color: "var(--color-primary-light)",
52
  },
53
  {
 
56
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
57
  </svg>
58
  ),
59
+ title: "即時分析",
60
+ description: "幾秒內獲得全面洞察,而非數小時。更多時間教學,更少時間評分。",
61
  color: "var(--color-accent-light)",
62
  },
63
  ];
 
67
  <div className="max-w-6xl mx-auto">
68
  <div className="text-center mb-16">
69
  <h2 className="font-display text-4xl font-bold text-[var(--color-text)] mb-4">
70
+ 了解您的學生所需的一切
71
+ <span className="text-gradient"> 功能</span>
72
  </h2>
73
  <p className="text-lg text-[var(--color-text-muted)] max-w-2xl mx-auto">
74
+ ClassLens 結合 AI 的力量與深思熟慮的教育實踐,為您提供可操作的洞察。
 
75
  </p>
76
  </div>
77
 
chatkit/frontend/src/components/Hero.tsx CHANGED
@@ -16,19 +16,18 @@ export function Hero({ isAuthenticated, onStartAnalysis }: HeroProps) {
16
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
18
  </svg>
19
- Powered by OpenAI ChatKit
20
  </span>
21
  </div>
22
 
23
  <h1 className="font-display text-5xl sm:text-6xl lg:text-7xl font-bold leading-tight mb-6 opacity-0 animate-fade-in-up stagger-2">
24
- <span className="text-[var(--color-text)]">Transform Exams into</span>
25
  <br />
26
- <span className="text-gradient">Teaching Insights</span>
27
  </h1>
28
 
29
  <p className="text-xl text-[var(--color-text-muted)] max-w-2xl mx-auto mb-10 opacity-0 animate-fade-in-up stagger-3">
30
- Upload your Google Form responses and let AI analyze student performance,
31
- generate personalized explanations, and create peer learning groups—all in seconds.
32
  </p>
33
 
34
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4 opacity-0 animate-fade-in-up stagger-4">
@@ -37,7 +36,7 @@ export function Hero({ isAuthenticated, onStartAnalysis }: HeroProps) {
37
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
38
  <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" />
39
  </svg>
40
- Start Analyzing
41
  </button>
42
  ) : (
43
  <a href="#connect" className="btn btn-primary text-lg px-8 py-4">
@@ -47,20 +46,20 @@ export function Hero({ isAuthenticated, onStartAnalysis }: HeroProps) {
47
  <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
48
  <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
49
  </svg>
50
- Connect Google Account
51
  </a>
52
  )}
53
  <a href="#features" className="btn btn-outline text-lg px-8 py-4">
54
- See How It Works
55
  </a>
56
  </div>
57
 
58
  {/* Stats */}
59
  <div className="mt-20 grid grid-cols-3 gap-8 max-w-2xl mx-auto opacity-0 animate-fade-in-up stagger-5">
60
  {[
61
- { value: "5min", label: "Average Analysis Time" },
62
- { value: "98%", label: "Teacher Satisfaction" },
63
- { value: "40+", label: "Question Types Supported" },
64
  ].map((stat, i) => (
65
  <div key={i} className="text-center">
66
  <div className="font-display text-3xl font-bold text-[var(--color-primary)]">
 
16
  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
18
  </svg>
19
+ OpenAI ChatKit 提供技術支援
20
  </span>
21
  </div>
22
 
23
  <h1 className="font-display text-5xl sm:text-6xl lg:text-7xl font-bold leading-tight mb-6 opacity-0 animate-fade-in-up stagger-2">
24
+ <span className="text-[var(--color-text)]">將考試轉化為</span>
25
  <br />
26
+ <span className="text-gradient">教學洞察</span>
27
  </h1>
28
 
29
  <p className="text-xl text-[var(--color-text-muted)] max-w-2xl mx-auto mb-10 opacity-0 animate-fade-in-up stagger-3">
30
+ 上傳您的 Google 表單回應,讓 AI 分析學生表現、生成個人化解釋並建立同儕學習小組——只需幾秒鐘。
 
31
  </p>
32
 
33
  <div className="flex flex-col sm:flex-row items-center justify-center gap-4 opacity-0 animate-fade-in-up stagger-4">
 
36
  <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
  <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" />
38
  </svg>
39
+ 開始分析
40
  </button>
41
  ) : (
42
  <a href="#connect" className="btn btn-primary text-lg px-8 py-4">
 
46
  <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
47
  <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
48
  </svg>
49
+ 連接 Google 帳號
50
  </a>
51
  )}
52
  <a href="#features" className="btn btn-outline text-lg px-8 py-4">
53
+ 查看使用方式
54
  </a>
55
  </div>
56
 
57
  {/* Stats */}
58
  <div className="mt-20 grid grid-cols-3 gap-8 max-w-2xl mx-auto opacity-0 animate-fade-in-up stagger-5">
59
  {[
60
+ { value: "5分鐘", label: "平均分析時間" },
61
+ { value: "98%", label: "教師滿意度" },
62
+ { value: "40+", label: "支援的題型" },
63
  ].map((stat, i) => (
64
  <div key={i} className="text-center">
65
  <div className="font-display text-3xl font-bold text-[var(--color-primary)]">
chatkit/frontend/src/index.css CHANGED
@@ -291,6 +291,20 @@ h1, h2, h3, h4, h5, h6 {
291
  background-clip: text;
292
  }
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  /* ChatKit customization */
295
  .chatkit-container {
296
  --ck-color-primary: var(--color-primary);
 
291
  background-clip: text;
292
  }
293
 
294
+ /* ChatKit customization - Force Traditional Chinese */
295
+ .chatkit-container,
296
+ [data-chatkit-composer] {
297
+ /* Force zh-TW locale */
298
+ }
299
+
300
+ /* Override ChatKit placeholder text to Traditional Chinese */
301
+ [data-chatkit-composer] textarea::placeholder,
302
+ [data-chatkit-composer] input::placeholder,
303
+ .chatkit-composer textarea::placeholder,
304
+ .chatkit-composer input::placeholder {
305
+ /* This may not work as ChatKit uses iframe */
306
+ }
307
+
308
  /* ChatKit customization */
309
  .chatkit-container {
310
  --ck-color-primary: var(--color-primary);
chatkit/run-local.sh ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Run ClassLens locally (both backend and frontend)
3
+
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ cd "$SCRIPT_DIR"
8
+
9
+ echo "🚀 Starting ClassLens locally..."
10
+ echo ""
11
+
12
+ # Check for .env file
13
+ if [ ! -f "../.env" ]; then
14
+ echo "⚠️ No .env file found. Please create one from env.example"
15
+ exit 1
16
+ fi
17
+
18
+ # Function to cleanup on exit
19
+ cleanup() {
20
+ echo ""
21
+ echo "🛑 Stopping servers..."
22
+ pkill -f "uvicorn app.main" 2>/dev/null || true
23
+ pkill -f "vite" 2>/dev/null || true
24
+ exit 0
25
+ }
26
+
27
+ trap cleanup SIGINT SIGTERM
28
+
29
+ # Start backend
30
+ echo "📦 Starting backend on http://127.0.0.1:8000"
31
+ cd backend
32
+ if [ ! -d ".venv" ]; then
33
+ echo "Creating Python virtual environment..."
34
+ python3 -m venv .venv
35
+ fi
36
+ source .venv/bin/activate
37
+ pip install -q -e . > /dev/null 2>&1
38
+
39
+ # Load environment variables
40
+ export $(grep -v '^#' ../.env | xargs)
41
+
42
+ # Start backend in background
43
+ uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload > /tmp/classlens-backend.log 2>&1 &
44
+ BACKEND_PID=$!
45
+
46
+ # Wait a bit for backend to start
47
+ sleep 2
48
+
49
+ # Start frontend
50
+ echo "🎨 Starting frontend on http://localhost:3000"
51
+ cd ../frontend
52
+
53
+ # Install frontend dependencies if needed
54
+ if [ ! -d "node_modules" ]; then
55
+ echo "Installing frontend dependencies..."
56
+ npm install
57
+ fi
58
+
59
+ # Start frontend
60
+ npm run dev > /tmp/classlens-frontend.log 2>&1 &
61
+ FRONTEND_PID=$!
62
+
63
+ echo ""
64
+ echo "✅ ClassLens is running!"
65
+ echo ""
66
+ echo " Backend: http://127.0.0.1:8000"
67
+ echo " Frontend: http://localhost:3000"
68
+ echo ""
69
+ echo " Backend logs: tail -f /tmp/classlens-backend.log"
70
+ echo " Frontend logs: tail -f /tmp/classlens-frontend.log"
71
+ echo ""
72
+ echo "Press Ctrl+C to stop both servers"
73
+ echo ""
74
+
75
+ # Wait for both processes
76
+ wait $BACKEND_PID $FRONTEND_PID