Files

533 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from 'next/server';
import { GeminiService } from '@/lib/services/gemini';
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;
const projectDescription = formData.get('projectDescription') as string;
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()) {
return NextResponse.json(
{ success: false, error: '請填寫專案標題' },
{ status: 400 }
);
}
if (!file && !websiteUrl?.trim()) {
return NextResponse.json(
{ success: false, error: '請上傳文件或提供網站連結' },
{ status: 400 }
);
}
// 獲取評分標準
console.log('📊 載入評分標準...');
const templates = await CriteriaItemService.getAllTemplates();
console.log('📊 載入的模板:', templates);
if (!templates || templates.length === 0) {
return NextResponse.json(
{ success: false, error: '未找到評分標準,請先設定評分標準' },
{ status: 400 }
);
}
const templateId = templates[0].id;
const criteria = templates[0].items || [];
console.log('📊 評分項目:', criteria);
// 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) {
// 檢查是否已有文件記錄(從前端傳入)
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 }
);
}
// 檢查文件大小 (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);
}
// 提取內容
content = await GeminiService.extractPPTContent(file);
} else if (websiteUrl) {
// 處理網站連結
console.log('🌐 開始處理網站連結...');
content = `網站內容分析: ${websiteUrl}
由於目前是簡化版本,無法直接抓取網站內容。
實際應用中應該使用網頁抓取技術來獲取網站內容進行分析。
專案描述: ${projectDescription || '無'}`;
}
// 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 || '',
geminiCriteria
);
const analysisDuration = Math.round((Date.now() - startTime) / 1000);
console.log(`⏱️ AI 分析耗時: ${analysisDuration}`);
// 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 / 100),
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 / 100),
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 cleanResultName = result.criteriaName.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
const criteriaItem = criteria.find(c => {
const cleanCriteriaName = c.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
return cleanCriteriaName === cleanResultName;
});
if (!criteriaItem) {
console.warn(`⚠️ 找不到對應的評分標準: "${result.criteriaName}"`);
continue;
}
// 合併 feedback 和 details 為一條記錄
let combinedContent = result.feedback;
if (result.details && result.details.trim()) {
combinedContent += `\n\n詳細說明${result.details}`;
}
await EvaluationFeedbackService.create({
evaluation_id: evaluationId,
criteria_item_id: criteriaItem.id,
feedback_type: 'criteria',
content: combinedContent,
sort_order: sortOrder++
});
console.log(`✅ 創建 criteria feedback: ${criteriaItem.name} (ID: ${criteriaItem.id})`);
}
// 如果有 fullData添加 strengths 和 improvements 反饋
if (evaluation.fullData) {
const fullData = evaluation.fullData;
// 為每個 criteria 添加 strengths 和 improvements
for (const criteriaData of fullData.criteria) {
// 使用清理後的名稱進行匹配
const cleanCriteriaDataName = criteriaData.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
const criteriaItem = criteria.find(c => {
const cleanCriteriaName = c.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
return cleanCriteriaName === cleanCriteriaDataName;
});
if (!criteriaItem) {
console.warn(`⚠️ 找不到對應的評分標準: "${criteriaData.name}" (清理後: "${cleanCriteriaDataName}")`);
continue;
}
console.log(`🔍 處理 criteria: "${criteriaData.name}" -> 匹配到: "${criteriaItem.name}" (ID: ${criteriaItem.id})`);
// 添加 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('📊 最終評分結果:');
console.log(' 總分:', evaluation.totalScore, '/', evaluation.maxTotalScore);
console.log(' 各項目評分:');
evaluation.results.forEach(result => {
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 ? {
name: file.name,
size: file.size,
type: file.type
} : null,
websiteUrl: websiteUrl || null
}
});
} catch (error) {
console.error('❌ 評審處理失敗:', error);
// 清理請求標識符(如果存在)
if (requestKey) {
processingRequests.delete(requestKey);
}
return NextResponse.json(
{ success: false, error: '評審處理失敗,請稍後再試' },
{ status: 500 }
);
}
}