diff --git a/ai/views.py b/ai/views.py
index 262d0fe..3725296 100644
--- a/ai/views.py
+++ b/ai/views.py
@@ -1,59 +1,151 @@
-from django.shortcuts import render
-from openai import OpenAI
+from google import genai
import os
from django.http import JsonResponse
-from django.views.decorators.csrf import csrf_exempt
-# from .models import DifficultyEvaluation, DifficultyEvaluationQuestion
+from django.contrib.auth.decorators import login_required
+from .models import DifficultyEvaluation
+from questions.models import Question, QuestionHistory
-client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
+client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
-@csrf_exempt
+def _payload_from_request(data):
+ return {
+ "title": data.get('title') or "",
+ "content": data.get('content') or "",
+ "level": data.get('level') or "",
+ "input_format": data.get('input_format') or "",
+ "output_format": data.get('output_format') or "",
+ "input_example": data.get('input_example') or "",
+ "output_example": data.get('output_example') or "",
+ "answer": data.get('answer') or "",
+ "hint": data.get('hint') or "",
+ "reference": data.get('reference') or "",
+ }
+
+@login_required
def analyze_question(request):
if request.method == "POST":
data = request.POST
- title = data.get('title')
- level = data.get('level')
- content = data.get('content')
- input_format = data.get('input_format')
- output_format = data.get('output_format')
- input_example = data.get('input_example')
- output_example = data.get('output_example')
- answer = data.get('answer')
- hint = data.get('hint')
- reference = data.get('reference')
- topics = data.getlist('topics')
- tags = data.getlist('tags')
-
- # 組合完整 prompt 給 AI
- full_question = (
- f"題目描述:{content}\n"
- f"輸入格式:{input_format}\n"
- f"輸出格式:{output_format}\n"
- f"輸入範例:{input_example}\n"
- f"輸出範例:{output_example}\n"
- f"提示:{hint}\n"
- )
-
try:
- completion = client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {"role": "system", "content": "你是一個用來評估程式設計題目難度的 AI 助理,請根據以下標準分析學生所出的題目,並給出:1.題目難度(簡單/中等/困難),2.評估依據(簡短說明使用哪些語法或概念),3,改進建議(可提供提升題目設計品質的建議,像是題目敘述完整度、方便閱讀程度、輸入/輸出格式表達正確度等等),4.題目標籤(格式為:#for迴圈、#函式等等)。難度評估標準如下:簡單:只需要使用基本的語法或概念,例如變數、條件判斷、迴圈等,若沒有跳出基本概念(如複雜的條件判斷)應判定為簡單。中等:需要使用較多的語法或概念,例如巢狀迴圈、陣列、字典、函式等。困難:需要使用複雜的語法或概念,例如遞迴、動態規劃、圖論、狀態轉移等。不要重複敘述題目內容和對解題方法給出建議,僅需要針對題目難度給出評估與建議。注意:如果你發現輸入內容是程式碼或是無關的文字而不是題目敘述,不要進行難度評估,僅回覆:「請提供題目敘述內容,才能進行難度分析喔 🙂」。題目可能以文字冒險、角色扮演、指令模擬等方式表達,不要因為語氣或敘事風格而誤判為非題目敘述,只要有明確任務與邏輯要求,即應視為程式設計題目。"},
- {"role": "user", "content": full_question},
- ],
- temperature=0.2,
+ # 整理 payload
+ payload = _payload_from_request(data)
+
+ # 建立/更新草稿 Question
+ draft_q = None
+ incoming_qid = data.get('question_id')
+ if incoming_qid:
+ try:
+ draft_q = Question.objects.get(id=incoming_qid)
+ # 覆寫草稿內容
+ for k, v in payload.items():
+ if hasattr(draft_q, k):
+ setattr(draft_q, k, v)
+ if hasattr(draft_q, "is_active"):
+ draft_q.is_active = False
+ if hasattr(draft_q, "is_approved"):
+ draft_q.is_approved = False
+ draft_q.save()
+ except Question.DoesNotExist:
+ draft_q = None
+
+ if draft_q is None:
+ # 第一次做 AI 分析:建一筆草稿
+ create_kwargs = {k: v for k, v in payload.items() if hasattr(Question, k)}
+ if hasattr(Question, "is_active"):
+ create_kwargs["is_active"] = False
+ if hasattr(Question, "is_approved"):
+ create_kwargs["is_approved"] = False
+
+ create_kwargs['user'] = request.user
+ draft_q = Question.objects.create(**create_kwargs)
+
+ if 'tags' in request.POST:
+ draft_q.tags.set(request.POST.getlist('tags'))
+ if 'topics' in request.POST:
+ draft_q.topics.set(request.POST.getlist('topics'))
+ draft_q.save()
+
+ # 建立 QuestionHistory
+ latest = QuestionHistory.objects.filter(question=draft_q).order_by('-version').first()
+ next_ver = (latest.version if latest else 0) + 1
+ hist = QuestionHistory.objects.create(
+ question=draft_q,
+ user=request.user if request.user.is_authenticated else None,
+ title=draft_q.title,
+ content=draft_q.content,
+ level=draft_q.level,
+ input_format=draft_q.input_format,
+ output_format=draft_q.output_format,
+ input_example=draft_q.input_example,
+ output_example=draft_q.output_example,
+ answer=getattr(draft_q, "answer", ""),
+ hint=getattr(draft_q, "hint", ""),
+ reference=getattr(draft_q, "reference", ""),
+ version=next_ver
)
- difficulty_content = completion.choices[0].message.content
-
- # # 將 AI 分析結果存到資料庫
- # difficulty_evaluation = DifficultyEvaluation.objects.create(
- # difficulty_score="待解析", # 可以後續解析 difficulty_content 來提取具體分數
- # feedback=difficulty_content
- # )
-
- return JsonResponse({'result': difficulty_content})
+ if hasattr(hist, "tags") and hasattr(draft_q, "tags"):
+ hist.tags.set(draft_q.tags.all())
+ if hasattr(hist, "topics") and hasattr(draft_q, "topics"):
+ hist.topics.set(draft_q.topics.all())
+
+ system_instruction = (
+ "你是一個用來評估程式設計題目難度的 AI 助理,請根據以下標準分析學生所出的題目,並給出:"
+ "1.題目難度(簡單/中等/困難),"
+ "2.評估依據(簡短說明使用哪些語法或概念),"
+ "3.改進建議(可提供提升題目設計品質的建議,像是題目敘述完整度、方便閱讀程度、輸入/輸出格式表達正確度等等),"
+ "4.題目標籤(格式為:#for迴圈、#函式等等)。"
+ "難度評估標準如下:"
+ "簡單:只需要使用基本的語法或概念,例如變數、條件判斷、迴圈等,若沒有跳出基本概念(如複雜的條件判斷)應判定為簡單。"
+ "中等:需要使用較多的語法或概念,例如巢狀迴圈、陣列、字典、函式等。"
+ "困難:需要使用複雜的語法或概念,例如遞迴、動態規劃、圖論、狀態轉移等。"
+ "不要重複敘述題目內容和對解題方法給出建議,僅需要針對題目難度給出評估與建議。"
+ "注意:如果你發現輸入內容是程式碼或是無關的文字而不是題目敘述,不要進行難度評估,僅回覆:「請提供題目敘述內容,才能進行難度分析喔 🙂」。"
+ "題目可能以文字冒險、角色扮演、指令模擬等方式表達,不要因為語氣或敘事風格而誤判為非題目敘述,只要有明確任務與邏輯要求,即應視為程式設計題目。"
+ )
+
+ user_message = (
+ f"題目標題:{payload['title']}\n"
+ f"題目描述:{payload['content']}\n"
+ f"輸入格式:{payload['input_format']}\n"
+ f"輸出格式:{payload['output_format']}\n"
+ f"輸入範例:{payload['input_example']}\n"
+ f"輸出範例:{payload['output_example']}\n"
+ f"提示:{payload['hint']}\n"
+ )
+
+ chat = client.chats.create(
+ model='gemini-2.5-flash',
+ config=genai.types.GenerateContentConfig(
+ system_instruction=system_instruction,
+ temperature=0.1
+ )
+ )
+
+ response = chat.send_message(user_message)
+ difficulty_content = response.text
+
+ # 難度判斷
+ difficulty_score = "未知"
+ if "困難" in difficulty_content:
+ difficulty_score = "困難"
+ elif "中等" in difficulty_content:
+ difficulty_score = "中等"
+ elif "簡單" in difficulty_content:
+ difficulty_score = "簡單"
+
+ difficulty_evaluation = DifficultyEvaluation.objects.create(
+ difficulty_score=difficulty_score,
+ feedback=difficulty_content
+ )
+
+ return JsonResponse({
+ 'result': difficulty_content,
+ 'evaluation_id': difficulty_evaluation.id,
+ 'difficulty_score': difficulty_score,
+ 'question_id': draft_q.id
+ })
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
return JsonResponse({'error': '只接受POST請求'}, status=400)
+
diff --git a/questions/views.py b/questions/views.py
index d81785e..19caeeb 100644
--- a/questions/views.py
+++ b/questions/views.py
@@ -6,21 +6,43 @@
from answers.models import Answer
from reviews.models import PeerReview
from .forms import QuestionForm, QuestionDetailForm
+from ai.models import DifficultyEvaluation, DifficultyEvaluationQuestion
from features.decorators import feature_required
@login_required
@feature_required('question_create')
def question_create(request):
if request.method == 'POST':
- form = QuestionForm(request.POST, user=request.user)
+ question_id = request.POST.get('question_id')
+ instance = None
+ if question_id:
+ try:
+ instance = Question.objects.get(id=question_id)
+ except Question.DoesNotExist:
+ instance = None
+ form = QuestionForm(request.POST, instance=instance, user=request.user)
if form.is_valid():
question = form.save()
+ # 建立與AI分析關聯
+ evaluation_id = request.POST.get('evaluation_id')
+ if evaluation_id:
+ try:
+ evaluation = DifficultyEvaluation.objects.get(id=evaluation_id)
+ DifficultyEvaluationQuestion.objects.create(
+ evaluation=evaluation,
+ question=question
+ )
+ except DifficultyEvaluation.DoesNotExist:
+ pass
# 記錄創建問題的日誌
QuestionLog.objects.create(
question=question,
user=request.user,
action='created'
)
+
+ latest = QuestionHistory.objects.filter(question=question).order_by('-version').first()
+ next_ver = (latest.version if latest else 0) + 1
# 創建問題的歷史記錄
history = QuestionHistory.objects.create(
question=question,
@@ -35,7 +57,7 @@ def question_create(request):
answer=question.answer,
hint=question.hint,
reference=question.reference,
- version=1
+ version=next_ver
)
# 設置多對多關係
history.tags.set(question.tags.all())
diff --git a/requirements.txt b/requirements.txt
index 79e368e..a2d930a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
django==5.2.3
psycopg2==2.9.10
pillow==11.2.1
-python-dotenv==1.0.1
\ No newline at end of file
+python-dotenv==1.0.1
+google-genai==1.31.0
+google-auth==2.40.3
diff --git a/static/js/questions/create.js b/static/js/questions/create.js
index 43700bc..3c81a88 100644
--- a/static/js/questions/create.js
+++ b/static/js/questions/create.js
@@ -351,6 +351,16 @@ function aiAnalysis() {
$("#ai-analysis-button").prop("disabled", true).text("AI分析完成").off("click");
showToast("AI分析完成", 'success');
+ // 儲存 evaluation_id 供後續題目提交時使用
+ if (response.evaluation_id) {
+ $("#questionForm input[name='evaluation_id']").remove();
+ $("#questionForm").append(``);
+ }
+ if (response.question_id) {
+ $("#questionForm input[name='question_id']").remove();
+ $("#questionForm").append(``);
+ }
+
// 恢復提交按鈕
$("button[type='submit']").prop("disabled", false).text("提交問題");
},