Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 137 additions & 45 deletions ai/views.py
Original file line number Diff line number Diff line change
@@ -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)

26 changes: 24 additions & 2 deletions questions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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())
Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
django==5.2.3
psycopg2==2.9.10
pillow==11.2.1
python-dotenv==1.0.1
python-dotenv==1.0.1
google-genai==1.31.0
google-auth==2.40.3
10 changes: 10 additions & 0 deletions static/js/questions/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<input type=\"hidden\" name=\"evaluation_id\" value=\"${response.evaluation_id}\">`);
}
if (response.question_id) {
$("#questionForm input[name='question_id']").remove();
$("#questionForm").append(`<input type="hidden" name="question_id" value="${response.question_id}">`);
}

// 恢復提交按鈕
$("button[type='submit']").prop("disabled", false).text("提交問題");
},
Expand Down