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("提交問題"); },