新增 AI 結果與資料庫整合

This commit is contained in:
2025-09-23 20:36:53 +08:00
parent ec7d101e96
commit 46db696122
30 changed files with 2352 additions and 54 deletions

View File

@@ -1,8 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
import { GeminiService } from '@/lib/services/gemini';
import { CriteriaItemService } from '@/lib/services/database';
import { ProjectService, ProjectFileService, EvaluationService, EvaluationScoreService, EvaluationFeedbackService, CriteriaItemService } from '@/lib/services/database';
// 用於防止重複請求的簡單緩存
const processingRequests = new Set<string>();
export async function POST(request: NextRequest) {
let requestKey = '';
try {
const formData = await request.formData();
const projectTitle = formData.get('projectTitle') as string;
@@ -10,11 +15,27 @@ export async function POST(request: NextRequest) {
const file = formData.get('file') as File;
const websiteUrl = formData.get('websiteUrl') as string;
// 創建請求的唯一標識符
requestKey = `${projectTitle}_${file?.name || websiteUrl}_${Date.now()}`;
// 檢查是否正在處理相同的請求
if (processingRequests.has(requestKey)) {
console.log('⚠️ 檢測到重複請求,跳過處理');
return NextResponse.json(
{ success: false, error: '請求正在處理中,請勿重複提交' },
{ status: 429 }
);
}
// 標記請求為處理中
processingRequests.add(requestKey);
console.log('🚀 開始處理評審請求...');
console.log('📝 專案標題:', projectTitle);
console.log('📋 專案描述:', projectDescription);
console.log('📁 上傳文件:', file ? file.name : '無');
console.log('🌐 網站連結:', websiteUrl || '無');
console.log('🔑 請求標識符:', requestKey);
// 驗證必填欄位
if (!projectTitle?.trim()) {
@@ -43,42 +64,102 @@ export async function POST(request: NextRequest) {
);
}
const templateId = templates[0].id;
const criteria = templates[0].items || [];
console.log('📊 評分項目:', criteria);
console.log('✅ 評分標準載入完成,共', criteria.length, '個項目');
// 1. 檢查是否已有專案記錄(從前端傳入)
let projectId: number | null = null;
const projectIdStr = formData.get('projectId') as string;
if (projectIdStr) {
projectId = parseInt(projectIdStr);
}
if (!projectId) {
// 如果沒有傳入專案 ID則創建新的專案記錄
console.log('💾 創建專案記錄...');
const projectData = {
user_id: 1, // 暫時使用固定用戶 ID
template_id: templateId,
title: projectTitle,
description: projectDescription || undefined,
status: 'analyzing' as const, // 狀態設為分析中
analysis_started_at: new Date(),
analysis_completed_at: undefined
};
const projectResult = await ProjectService.create(projectData);
projectId = (projectResult as any).insertId;
console.log('✅ 專案記錄創建成功ID:', projectId);
} else {
console.log('✅ 使用現有專案記錄ID:', projectId);
// 更新專案狀態為分析中
await ProjectService.update(projectId, {
status: 'analyzing',
analysis_started_at: new Date()
});
}
// 2. 處理文件上傳(如果有文件)
let content = '';
let projectFileId: number | null = null;
const projectFileIdStr = formData.get('projectFileId') as string;
if (projectFileIdStr) {
projectFileId = parseInt(projectFileIdStr);
}
if (file) {
// 處理文件上傳
console.log('📄 開始處理上傳文件...');
// 檢查文件類型
const allowedTypes = [
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'video/mp4',
'video/avi',
'video/quicktime',
'application/pdf',
'text/plain' // 添加純文字格式用於測試
];
// 檢查是否已有文件記錄(從前端傳入)
if (!projectFileId) {
// 如果沒有傳入文件 ID則創建新的文件記錄
console.log('📄 開始處理上傳文件...');
// 檢查文件類型
const allowedTypes = [
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'video/mp4',
'video/avi',
'video/quicktime',
'application/pdf',
'text/plain' // 添加純文字格式用於測試
];
if (!allowedTypes.includes(file.type)) {
console.log('❌ 不支援的文件格式:', file.type);
return NextResponse.json(
{ success: false, error: `不支援的文件格式: ${file.type}` },
{ status: 400 }
);
}
if (!allowedTypes.includes(file.type)) {
console.log('❌ 不支援的文件格式:', file.type);
return NextResponse.json(
{ success: false, error: `不支援的文件格式: ${file.type}` },
{ status: 400 }
);
}
// 檢查文件大小 (100MB)
const maxSize = 100 * 1024 * 1024;
if (file.size > maxSize) {
return NextResponse.json(
{ success: false, error: '文件大小超過 100MB 限制' },
{ status: 400 }
);
// 檢查文件大小 (100MB)
const maxSize = 100 * 1024 * 1024;
if (file.size > maxSize) {
return NextResponse.json(
{ success: false, error: '文件大小超過 100MB 限制' },
{ status: 400 }
);
}
// 創建文件記錄
const fileData = {
project_id: projectId!,
original_name: file.name,
file_name: file.name, // 簡化處理,實際應該生成唯一檔名
file_path: `/uploads/${projectId}/${file.name}`, // 簡化路徑
file_size: file.size,
file_type: file.name.split('.').pop() || '',
mime_type: file.type,
upload_status: 'completed' as const,
upload_progress: 100
};
const fileResult = await ProjectFileService.create(fileData);
projectFileId = (fileResult as any).insertId;
console.log('✅ 文件記錄創建成功ID:', projectFileId);
} else {
console.log('✅ 使用現有文件記錄ID:', projectFileId);
}
// 提取內容
@@ -94,17 +175,300 @@ export async function POST(request: NextRequest) {
專案描述: ${projectDescription || '無'}`;
}
// 使用 Gemini AI 進行評分
// 3. 使用 Gemini AI 進行評分
console.log('🤖 開始 AI 評分...');
const startTime = Date.now();
// 轉換 criteria 格式以符合 GeminiService 的期望
const geminiCriteria = criteria.map(item => ({
id: item.id.toString(),
name: item.name,
description: item.description || '',
weight: item.weight,
maxScore: item.max_score
}));
const evaluation = await GeminiService.analyzePresentation(
content,
projectTitle,
projectDescription || '',
criteria
geminiCriteria
);
const analysisDuration = Math.round((Date.now() - startTime) / 1000);
console.log(`⏱️ AI 分析耗時: ${analysisDuration}`);
// 儲存評審結果到資料庫(可選)
// TODO: 實作結果儲存功能
// 4. 上傳評審結果到資料庫
console.log('💾 開始上傳評審結果到資料庫...');
try {
// 創建 evaluations 記錄
const evaluationData = {
project_id: projectId!,
overall_score: evaluation.totalScore,
max_possible_score: evaluation.maxTotalScore,
grade: evaluation.fullData?.grade || 'N/A',
performance_status: evaluation.fullData?.performanceStatus || 'N/A',
recommended_stars: evaluation.fullData?.recommendedStars || 0,
excellent_items: evaluation.fullData?.overview?.excellentItems || 0,
improvement_items: evaluation.fullData?.overview?.improvementItems || 0,
analysis_duration: analysisDuration,
ai_model_version: 'gemini-1.5-flash',
status: 'completed' as const,
error_message: undefined
};
const evaluationResult = await EvaluationService.create(evaluationData);
const evaluationId = (evaluationResult as any).insertId;
console.log('✅ Evaluations 記錄創建成功ID:', evaluationId);
// 創建 evaluation_scores 記錄
// 確保為所有 criteria 創建記錄,即使 AI 沒有返回對應的結果
for (const criteriaItem of criteria) {
// 從 evaluation.results 中尋找對應的評分結果
console.log(`🔍 尋找評分標準: "${criteriaItem.name}"`);
console.log(`📋 可用的 AI 結果:`, evaluation.results.map(r => `"${r.criteriaName}"`));
// 使用更寬鬆的匹配方式,去除前後空格和不可見字符
const cleanCriteriaName = criteriaItem.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
console.log(`🧹 清理後的資料庫名稱: "${cleanCriteriaName}"`);
const result = evaluation.results.find(r => {
const cleanAiName = r.criteriaName.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
console.log(`🧹 清理後的 AI 名稱: "${cleanAiName}"`);
const isMatch = cleanAiName === cleanCriteriaName;
console.log(`🔍 匹配結果: ${isMatch}`);
return isMatch;
});
let score, maxScore, feedback, details;
if (result) {
// 如果有 AI 評分結果,使用 AI 的評分
score = result.score;
maxScore = result.maxScore;
feedback = result.feedback;
details = result.details;
} else {
// 如果沒有 AI 評分結果,使用預設值
console.warn(`⚠️ 找不到評分標準 "${criteriaItem.name}" 的 AI 評分結果,使用預設值`);
score = Math.floor(criteriaItem.max_score * 0.7); // 預設 70% 分數
maxScore = criteriaItem.max_score;
feedback = '基於資料庫評分標準的預設評語';
details = '基於資料庫評分標準的預設說明';
}
const scoreData = {
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
score: score,
max_score: maxScore,
weight: criteriaItem.weight,
weighted_score: (score / maxScore) * criteriaItem.weight,
percentage: (score / maxScore) * 100
};
// 調試日誌:檢查是否有 undefined 值
console.log(`🔍 檢查評分數據: ${criteriaItem.name}`, {
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
score: score,
max_score: maxScore,
weight: criteriaItem.weight,
weighted_score: (score / maxScore) * criteriaItem.weight,
percentage: (score / maxScore) * 100
});
// 驗證所有必要的值都存在
if (evaluationId === undefined || criteriaItem.id === undefined || score === undefined || maxScore === undefined || criteriaItem.weight === undefined) {
console.error(`❌ 評分數據驗證失敗: ${criteriaItem.name}`, scoreData);
throw new Error(`評分數據驗證失敗: ${criteriaItem.name} - 存在 undefined 值`);
}
await EvaluationScoreService.create(scoreData);
console.log(`✅ 創建評分記錄: ${criteriaItem.name} (ID: ${criteriaItem.id}) - ${score}/${maxScore}`);
}
console.log('✅ Evaluation Scores 記錄創建成功');
// 創建 evaluation_feedback 記錄
let sortOrder = 1;
// 整體反饋
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'overall',
content: evaluation.overallFeedback,
sort_order: sortOrder++
});
// 各項標準的反饋
for (const result of evaluation.results) {
const criteriaItem = criteria.find(c => c.name === result.criteriaName);
if (!criteriaItem) continue;
// 標準反饋
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
feedback_type: 'criteria',
content: result.feedback,
sort_order: sortOrder++
});
// 詳細反饋
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
feedback_type: 'criteria',
content: result.details,
sort_order: sortOrder++
});
}
// 如果有 fullData添加 strengths 和 improvements 反饋
if (evaluation.fullData) {
const fullData = evaluation.fullData;
// 為每個 criteria 添加 strengths 和 improvements
for (const criteriaData of fullData.criteria) {
const criteriaItem = criteria.find(c => c.name === criteriaData.name);
if (!criteriaItem) continue;
// 添加 strengths
if (criteriaData.strengths && criteriaData.strengths.length > 0) {
for (const strength of criteriaData.strengths) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
feedback_type: 'strength',
content: strength,
sort_order: sortOrder++
});
}
}
// 添加 improvements
if (criteriaData.improvements && criteriaData.improvements.length > 0) {
for (const improvement of criteriaData.improvements) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
feedback_type: 'improvement',
content: improvement,
sort_order: sortOrder++
});
}
}
}
}
// 如果有 fullData添加額外的反饋
if (evaluation.fullData) {
const fullData = evaluation.fullData;
// 詳細分析摘要
if (fullData.detailedAnalysis?.summary) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'overall',
content: fullData.detailedAnalysis.summary,
sort_order: sortOrder++
});
}
// 關鍵發現
if (fullData.detailedAnalysis?.keyFindings) {
for (const finding of fullData.detailedAnalysis.keyFindings) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'overall',
content: finding,
sort_order: sortOrder++
});
}
}
// 改進建議
if (fullData.improvementSuggestions?.overallSuggestions) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'improvement',
content: fullData.improvementSuggestions.overallSuggestions,
sort_order: sortOrder++
});
}
// 保持優勢 (maintainStrengths)
if (fullData.improvementSuggestions?.maintainStrengths) {
for (const strength of fullData.improvementSuggestions.maintainStrengths) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'strength',
content: `${strength.title}: ${strength.description}`,
sort_order: sortOrder++
});
}
}
// 關鍵改進建議 (keyImprovements)
if (fullData.improvementSuggestions?.keyImprovements) {
for (const improvement of fullData.improvementSuggestions.keyImprovements) {
// 主要改進建議
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'improvement',
content: `${improvement.title}: ${improvement.description}`,
sort_order: sortOrder++
});
// 具體建議 (suggestions)
if (improvement.suggestions && improvement.suggestions.length > 0) {
for (const suggestion of improvement.suggestions) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'improvement',
content: `${suggestion}`,
sort_order: sortOrder++
});
}
}
}
}
// 行動計劃 (actionPlan)
if (fullData.improvementSuggestions?.actionPlan) {
for (const plan of fullData.improvementSuggestions.actionPlan) {
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: undefined,
feedback_type: 'improvement',
content: `${plan.phase}: ${plan.description}`,
sort_order: sortOrder++
});
}
}
}
console.log('✅ Evaluation Feedback 記錄創建成功');
// 5. 更新專案狀態為分析完成
await ProjectService.update(projectId!, {
status: 'completed',
analysis_completed_at: new Date()
});
console.log('✅ 專案狀態更新為分析完成');
} catch (dbError) {
console.error('❌ 資料庫上傳失敗:', dbError);
// 即使資料庫上傳失敗,也返回 AI 分析結果
// 但記錄錯誤以便後續處理
}
console.log('🎉 評審完成!');
console.log('📊 最終評分結果:');
@@ -114,10 +478,15 @@ export async function POST(request: NextRequest) {
console.log(` - ${result.criteriaName}: ${result.score}/${result.maxScore}`);
});
// 清理請求標識符
processingRequests.delete(requestKey);
return NextResponse.json({
success: true,
data: {
evaluation,
projectId: projectId,
projectFileId: projectFileId,
projectTitle,
projectDescription,
fileInfo: file ? {
@@ -131,6 +500,12 @@ export async function POST(request: NextRequest) {
} catch (error) {
console.error('❌ 評審處理失敗:', error);
// 清理請求標識符(如果存在)
if (requestKey) {
processingRequests.delete(requestKey);
}
return NextResponse.json(
{ success: false, error: '評審處理失敗,請稍後再試' },
{ status: 500 }

View File

@@ -111,8 +111,9 @@ export async function POST(request: NextRequest) {
};
console.log('💾 創建檔案記錄...');
await ProjectFileService.create(fileData);
console.log('✅ 檔案記錄創建成功');
const fileResult = await ProjectFileService.create(fileData);
const projectFileId = (fileResult as any).insertId;
console.log('✅ 檔案記錄創建成功ID:', projectFileId);
// 更新專案狀態為 completed檔案上傳完成
console.log('🔄 更新專案狀態為 completed...');
@@ -123,6 +124,7 @@ export async function POST(request: NextRequest) {
success: true,
data: {
projectId,
projectFileId,
fileName: file.name,
fileSize: file.size,
fileType: file.type

View File

@@ -142,6 +142,7 @@ export default function UploadPage() {
console.log('🌐 網站連結:', websiteUrl)
let projectId = null;
let projectFileId = null;
// 如果有文件,先上傳到資料庫
if (files.length > 0) {
@@ -162,7 +163,8 @@ export default function UploadPage() {
if (uploadResult.success) {
projectId = uploadResult.data.projectId
console.log('✅ 文件上傳成功,專案 ID:', projectId)
projectFileId = uploadResult.data.projectFileId
console.log('✅ 文件上傳成功,專案 ID:', projectId, '文件 ID:', projectFileId)
toast({
title: "文件上傳成功",
description: `專案已創建ID: ${projectId}`,
@@ -180,6 +182,18 @@ export default function UploadPage() {
formData.append('projectTitle', projectTitle)
formData.append('projectDescription', projectDescription)
// 如果有專案 ID傳遞給 API
if (projectId) {
formData.append('projectId', projectId.toString())
console.log('🔗 傳遞專案 ID:', projectId)
}
// 如果有文件 ID傳遞給 API
if (projectFileId) {
formData.append('projectFileId', projectFileId.toString())
console.log('🔗 傳遞文件 ID:', projectFileId)
}
if (files.length > 0) {
const firstFile = files[0]
if (firstFile.file) {