-
Notifications
You must be signed in to change notification settings - Fork 0
feat: #98 AI生成演習の結果表示画面を実装 #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,15 +11,15 @@ | |
|
|
||
| async def generate_handson_quest( | ||
| document_content: str, | ||
| user_rank: int, | ||
| user_rank: int = 0, | ||
| user_skills: str = "", | ||
| ) -> dict: | ||
| """ | ||
| LLMを使用してハンズオン演習を生成 | ||
|
|
||
| Args: | ||
| document_content: 学習対象のドキュメント | ||
| user_rank: ユーザーのランク(0-9) | ||
| user_rank: ユーザーのランク(0-9、デフォルト=0) | ||
| user_skills: ユーザーの得意分野(オプション) | ||
|
|
||
| Returns: | ||
|
|
@@ -46,17 +46,30 @@ async def generate_handson_quest( | |
| # LLMに非同期で呼び出し (temperature=0.7で創造性を持たせる) | ||
| response = await invoke_llm(prompt=prompt, temperature=0.7) | ||
|
|
||
| # Markdownコードブロックを除去(LLMが```json ... ```で囲む場合がある) | ||
| cleaned_response = response.strip() | ||
| if cleaned_response.startswith("```json"): | ||
| cleaned_response = cleaned_response[7:] # "```json" を削除 | ||
| elif cleaned_response.startswith("```"): | ||
| cleaned_response = cleaned_response[3:] # "```" を削除 | ||
| if cleaned_response.endswith("```"): | ||
| cleaned_response = cleaned_response[:-3] # 末尾の "```" を削除 | ||
| cleaned_response = cleaned_response.strip() | ||
|
|
||
| # JSONパース(エラーハンドリング付き) | ||
| try: | ||
| result = json.loads(response) | ||
| result = json.loads(cleaned_response) | ||
| # 必須フィールドの存在確認 | ||
| required_fields = ["title", "difficulty", "steps"] | ||
| if not all(k in result for k in required_fields): | ||
| raise ValueError("Missing required fields in LLM response") | ||
| return result | ||
| except (json.JSONDecodeError, ValueError) as e: | ||
| # LLMがJSON以外を返した場合のフォールバック | ||
| print(f"JSON parse error: {e}. Returning fallback response.") | ||
| print(f"❌ JSON parse error: {e}") | ||
| print(f"📝 LLM Response (first 500 chars): {response[:500]}") | ||
| print(f"📝 LLM Response (last 500 chars): {response[-500:]}") | ||
| print("⚠️ Returning fallback response.") | ||
|
Comment on lines
+69
to
+72
|
||
| return { | ||
| "title": "演習生成エラー", | ||
| "difficulty": "beginner", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import { useRouter } from "next/navigation"; | ||
| import { withAuth } from "@/lib/auth/withAuth"; | ||
| import type { QuestGenerationResponse } from "@/features/exercise/types"; | ||
| import { QuestResultHeader } from "@/features/exercise/components/QuestResultHeader"; | ||
| import { QuestObjectives } from "@/features/exercise/components/QuestObjectives"; | ||
| import { QuestStepList } from "@/features/exercise/components/QuestStepList"; | ||
| import { QuestResources } from "@/features/exercise/components/QuestResources"; | ||
|
|
||
| function ExerciseGenerateResultPage() { | ||
| const router = useRouter(); | ||
| const [quest, setQuest] = useState<QuestGenerationResponse | null>(null); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| const loadQuest = () => { | ||
| const raw = sessionStorage.getItem("questGenerationResult"); | ||
| if (!raw) { | ||
| setError( | ||
| "生成結果が見つかりません。演習生成ページからやり直してください。", | ||
| ); | ||
| return; | ||
| } | ||
| try { | ||
| setQuest(JSON.parse(raw) as QuestGenerationResponse); | ||
|
Comment on lines
+18
to
+27
|
||
| } catch { | ||
| setError( | ||
| "データの読み込みに失敗しました。演習生成ページからやり直してください。", | ||
| ); | ||
| } | ||
| }; | ||
|
|
||
| loadQuest(); | ||
| }, []); | ||
|
|
||
| // エラー状態 | ||
| if (error) { | ||
| return ( | ||
| <div className="min-h-screen bg-[#FDFEF0] flex items-center justify-center"> | ||
| <div className="text-center max-w-md mx-auto px-6"> | ||
| <div | ||
| className="bg-white border-4 border-red-500 p-8 mb-6" | ||
| style={{ boxShadow: "6px 6px 0 #000" }} | ||
| > | ||
| <p className="text-red-600 font-bold text-xl mb-3">⚠️ エラー</p> | ||
| <p className="text-[#14532D]">{error}</p> | ||
| </div> | ||
| <button | ||
| onClick={() => router.push("/exercises")} | ||
| className="inline-flex items-center gap-2 px-6 py-3 bg-[#FDFEF0] border-4 border-[#14532D] text-[#14532D] font-bold hover:bg-[#4ADE80] transition-colors" | ||
| style={{ boxShadow: "4px 4px 0 #000" }} | ||
| > | ||
| <span className="text-xl">←</span> | ||
| 演習生成ページへ戻る | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // ローディング状態 | ||
| if (!quest) { | ||
| return ( | ||
| <div className="min-h-screen bg-[#FDFEF0] flex items-center justify-center"> | ||
| <div className="text-[#14532D] text-2xl font-bold">Loading...</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="min-h-screen bg-[#FDFEF0] p-8"> | ||
| <div className="max-w-5xl mx-auto"> | ||
| {/* 戻るボタン */} | ||
| <div className="mb-8"> | ||
| <button | ||
| onClick={() => router.push("/exercises")} | ||
| className="inline-flex items-center gap-2 px-6 py-3 bg-[#FDFEF0] border-4 border-[#14532D] text-[#14532D] font-bold hover:bg-[#4ADE80] transition-colors mb-6" | ||
| style={{ boxShadow: "4px 4px 0 #000" }} | ||
| > | ||
| <span className="text-xl">←</span> | ||
| <span>戻る</span> | ||
| </button> | ||
|
|
||
| {/* ヘッダー */} | ||
| <QuestResultHeader quest={quest} /> | ||
| </div> | ||
|
|
||
| {/* 学習目標 */} | ||
| <QuestObjectives objectives={quest.learning_objectives} /> | ||
|
|
||
| {/* ステップ一覧 */} | ||
| <QuestStepList steps={quest.steps} /> | ||
|
|
||
| {/* 参考リソース */} | ||
| <QuestResources resources={quest.resources} /> | ||
|
|
||
| {/* 底部アクション */} | ||
| <div className="flex flex-wrap gap-4 mt-8"> | ||
| <button | ||
| onClick={() => { | ||
| sessionStorage.removeItem("questGenerationResult"); | ||
| router.push("/exercises"); | ||
| }} | ||
| className="inline-flex items-center gap-2 px-6 py-3 bg-[#4ADE80] border-4 border-[#14532D] text-[#14532D] font-bold hover:bg-[#86EFAC] transition-colors" | ||
| style={{ boxShadow: "4px 4px 0 #000" }} | ||
| > | ||
| <span>🔄</span> | ||
| もう一度生成する | ||
| </button> | ||
| <button | ||
| onClick={() => router.push("/exercises")} | ||
| className="inline-flex items-center gap-2 px-6 py-3 bg-[#FDFEF0] border-4 border-[#14532D] text-[#14532D] font-bold hover:bg-[#4ADE80] transition-colors" | ||
| style={{ boxShadow: "4px 4px 0 #000" }} | ||
| > | ||
| <span>🎮</span> | ||
| 演習一覧へ | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default withAuth(ExerciseGenerateResultPage); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
必須フィールドチェックが title/difficulty/steps のみのため、LLMが estimated_time_minutes や learning_objectives を欠いたJSONを返すと、この関数は成功扱いで返却し、呼び出し側の QuestGenerationResponse 検証で 500 になり得ます。ここで QuestGenerationResponse の必須項目(estimated_time_minutes, learning_objectives, resources など)も検証するか、欠損時はデフォルト値補完/フォールバックに落とすようにして、500 を避けてください。