新增 AI 解析、評分結果
This commit is contained in:
@@ -129,6 +129,56 @@ export class CriteriaItemService {
|
||||
return result;
|
||||
}
|
||||
|
||||
static async getAllTemplates(): Promise<CriteriaTemplateWithItems[]> {
|
||||
const sql = `
|
||||
SELECT t.*,
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', i.id,
|
||||
'name', i.name,
|
||||
'description', i.description,
|
||||
'weight', i.weight,
|
||||
'maxScore', i.max_score,
|
||||
'sort_order', i.sort_order
|
||||
)
|
||||
) as items
|
||||
FROM criteria_templates t
|
||||
LEFT JOIN criteria_items i ON t.id = i.template_id
|
||||
GROUP BY t.id
|
||||
ORDER BY t.created_at DESC
|
||||
`;
|
||||
|
||||
const rows = await query(sql) as any[];
|
||||
|
||||
return rows.map(row => {
|
||||
let items = [];
|
||||
if (row.items) {
|
||||
try {
|
||||
// 檢查 items 是否已經是對象數組
|
||||
if (Array.isArray(row.items)) {
|
||||
items = row.items;
|
||||
} else if (typeof row.items === 'string') {
|
||||
items = JSON.parse(row.items);
|
||||
} else {
|
||||
console.log('items 類型:', typeof row.items, row.items);
|
||||
items = [];
|
||||
}
|
||||
// 手動排序項目
|
||||
items.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
|
||||
} catch (error) {
|
||||
console.error('解析 items JSON 失敗:', error);
|
||||
console.error('原始 items 數據:', row.items);
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...row,
|
||||
items
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static async findByTemplateId(templateId: number): Promise<CriteriaItem[]> {
|
||||
const sql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order';
|
||||
const rows = await query(sql, [templateId]) as any[];
|
||||
|
769
lib/services/gemini.ts
Normal file
769
lib/services/gemini.ts
Normal file
@@ -0,0 +1,769 @@
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
|
||||
const API_KEY = 'AIzaSyAN3pEJr_Vn2xkCidGZAq9eQqsMVvpj8g4';
|
||||
const genAI = new GoogleGenerativeAI(API_KEY);
|
||||
|
||||
export interface CriteriaItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
weight: number;
|
||||
maxScore: number;
|
||||
}
|
||||
|
||||
export interface ScoringResult {
|
||||
criteriaId: string;
|
||||
criteriaName: string;
|
||||
score: number;
|
||||
maxScore: number;
|
||||
feedback: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
export interface ProjectEvaluation {
|
||||
projectTitle: string;
|
||||
projectDescription: string;
|
||||
totalScore: number;
|
||||
maxTotalScore: number;
|
||||
results: ScoringResult[];
|
||||
overallFeedback: string;
|
||||
fullData?: {
|
||||
projectTitle: string;
|
||||
overallScore: number;
|
||||
totalPossible: number;
|
||||
grade: string;
|
||||
performanceStatus: string;
|
||||
recommendedStars: number;
|
||||
analysisDate: string;
|
||||
criteria: any[];
|
||||
overview: {
|
||||
excellentItems: number;
|
||||
improvementItems: number;
|
||||
overallPerformance: number;
|
||||
};
|
||||
detailedAnalysis: {
|
||||
summary: string;
|
||||
keyFindings: string[];
|
||||
};
|
||||
chartData: {
|
||||
barChart: any[];
|
||||
pieChart: any[];
|
||||
radarChart: any[];
|
||||
};
|
||||
improvementSuggestions: {
|
||||
overallSuggestions: string;
|
||||
maintainStrengths: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
keyImprovements: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
suggestions: string[];
|
||||
}>;
|
||||
actionPlan: Array<{
|
||||
phase: string;
|
||||
description: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export class GeminiService {
|
||||
private static model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
||||
|
||||
/**
|
||||
* 分析 PPT 內容並進行評分
|
||||
*/
|
||||
static async analyzePresentation(
|
||||
pptContent: string,
|
||||
projectTitle: string,
|
||||
projectDescription: string,
|
||||
criteria: CriteriaItem[]
|
||||
): Promise<ProjectEvaluation> {
|
||||
try {
|
||||
const prompt = this.buildScoringPrompt(pptContent, projectTitle, projectDescription, criteria);
|
||||
|
||||
console.log('🤖 開始使用 Gemini AI 分析 PPT 內容...');
|
||||
console.log('📝 專案標題:', projectTitle);
|
||||
console.log('📋 評分標準數量:', criteria.length);
|
||||
|
||||
const result = await this.model.generateContent(prompt);
|
||||
const response = await result.response;
|
||||
const text = response.text();
|
||||
|
||||
console.log('✅ Gemini AI 分析完成');
|
||||
console.log('📊 原始回應:', text);
|
||||
console.log('📊 評分標準:', criteria.map(c => c.name));
|
||||
|
||||
// 解析 Gemini 的回應
|
||||
const evaluation = this.parseGeminiResponse(text, criteria);
|
||||
|
||||
console.log('🎯 評分結果:');
|
||||
console.log(' 總分:', evaluation.totalScore, '/', evaluation.maxTotalScore);
|
||||
console.log(' 各項目分數:');
|
||||
evaluation.results.forEach(result => {
|
||||
console.log(` - ${result.criteriaName}: ${result.score}/${result.maxScore} (${result.feedback})`);
|
||||
});
|
||||
|
||||
console.log('📊 最終 evaluation 對象:', JSON.stringify(evaluation, null, 2));
|
||||
|
||||
return evaluation;
|
||||
} catch (error) {
|
||||
console.error('❌ Gemini AI 分析失敗:', error);
|
||||
throw new Error('AI 分析失敗,請稍後再試');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 構建評分提示詞
|
||||
*/
|
||||
private static buildScoringPrompt(
|
||||
pptContent: string,
|
||||
projectTitle: string,
|
||||
projectDescription: string,
|
||||
criteria: CriteriaItem[]
|
||||
): string {
|
||||
const criteriaList = criteria.map((item, index) =>
|
||||
`${index + 1}. ${item.name} (權重: ${item.weight}%, 滿分: ${item.maxScore}分)
|
||||
說明: ${item.description}`
|
||||
).join('\n');
|
||||
|
||||
return `
|
||||
你是一位專業的評審專家,請根據以下評分標準對 PPT 內容進行詳細評分。
|
||||
|
||||
**專案資訊:**
|
||||
- 專案標題: ${projectTitle}
|
||||
- 專案描述: ${projectDescription}
|
||||
|
||||
**PPT 內容:**
|
||||
${pptContent}
|
||||
|
||||
**評分標準:**
|
||||
${criteriaList}
|
||||
|
||||
**評分要求:**
|
||||
1. 請對每個評分項目給出 0 到滿分的分數
|
||||
2. 為每個項目提供具體的評分理由、優點和改進建議
|
||||
3. 計算總分(各項目分數 × 權重比例)
|
||||
4. 提供整體評價和建議
|
||||
5. 分析優秀項目和待改進項目的數量
|
||||
6. 給出等級評比 (S、A+、A、A-、B+、B、B-、C、D)
|
||||
7. 給出表現狀態 (表現極優、表現良好、表現普通、表現有待加強)
|
||||
8. 給出推薦星星數量 (1-5顆星)
|
||||
|
||||
**回應格式 (請嚴格按照以下 JSON 格式回應):**
|
||||
{
|
||||
"projectTitle": "專案標題",
|
||||
"overallScore": 總分數字,
|
||||
"totalPossible": 100,
|
||||
"grade": "等級評比",
|
||||
"performanceStatus": "表現狀態",
|
||||
"recommendedStars": 推薦星星數量,
|
||||
"analysisDate": "當前日期 (YYYY-MM-DD 格式)",
|
||||
"criteria": [
|
||||
{
|
||||
"name": "項目名稱",
|
||||
"score": 得分數字,
|
||||
"maxScore": 滿分數字,
|
||||
"weight": 權重百分比,
|
||||
"weightedScore": 加權分數,
|
||||
"feedback": "AI 評語",
|
||||
"strengths": ["優點1", "優點2", "優點3"],
|
||||
"improvements": ["改進建議1", "改進建議2"]
|
||||
}
|
||||
],
|
||||
"overview": {
|
||||
"excellentItems": 優秀項目數量,
|
||||
"improvementItems": 待改進項目數量,
|
||||
"overallPerformance": 整體表現百分比
|
||||
},
|
||||
"detailedAnalysis": {
|
||||
"summary": "整體分析摘要",
|
||||
"keyFindings": ["關鍵發現1", "關鍵發現2", "關鍵發現3"]
|
||||
},
|
||||
"chartData": {
|
||||
"barChart": [
|
||||
{
|
||||
"name": "項目名稱",
|
||||
"score": 得分,
|
||||
"maxScore": 滿分,
|
||||
"percentage": 百分比
|
||||
}
|
||||
],
|
||||
"pieChart": [
|
||||
{
|
||||
"name": "項目名稱",
|
||||
"value": 加權分數,
|
||||
"weight": 權重
|
||||
}
|
||||
],
|
||||
"radarChart": [
|
||||
{
|
||||
"subject": "項目名稱",
|
||||
"score": 得分,
|
||||
"fullMark": 滿分
|
||||
}
|
||||
]
|
||||
},
|
||||
"improvementSuggestions": {
|
||||
"overallSuggestions": "整體改進建議",
|
||||
"maintainStrengths": [
|
||||
{
|
||||
"title": "優勢標題",
|
||||
"description": "優勢描述"
|
||||
}
|
||||
],
|
||||
"keyImprovements": [
|
||||
{
|
||||
"title": "改進標題",
|
||||
"description": "改進描述",
|
||||
"suggestions": ["建議1", "建議2", "建議3"]
|
||||
}
|
||||
],
|
||||
"actionPlan": [
|
||||
{
|
||||
"phase": "短期目標(1-2週)",
|
||||
"description": "行動描述"
|
||||
},
|
||||
{
|
||||
"phase": "中期目標(1個月)",
|
||||
"description": "行動描述"
|
||||
},
|
||||
{
|
||||
"phase": "長期目標(3個月)",
|
||||
"description": "行動描述"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
請確保回應是有效的 JSON 格式,不要包含任何其他文字。
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算等級評比
|
||||
*/
|
||||
private static calculateGrade(score: number): string {
|
||||
if (score >= 95) return 'S';
|
||||
if (score >= 90) return 'A+';
|
||||
if (score >= 85) return 'A';
|
||||
if (score >= 80) return 'A-';
|
||||
if (score >= 75) return 'B+';
|
||||
if (score >= 70) return 'B';
|
||||
if (score >= 65) return 'B-';
|
||||
if (score >= 60) return 'C';
|
||||
return 'D';
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算表現狀態
|
||||
*/
|
||||
private static calculatePerformanceStatus(score: number): string {
|
||||
if (score >= 90) return '表現極優';
|
||||
if (score >= 80) return '表現良好';
|
||||
if (score >= 70) return '表現普通';
|
||||
return '表現有待加強';
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算推薦星星數量
|
||||
*/
|
||||
private static calculateRecommendedStars(score: number): number {
|
||||
if (score >= 90) return 5;
|
||||
if (score >= 80) return 4;
|
||||
if (score >= 70) return 3;
|
||||
if (score >= 60) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算總覽統計
|
||||
*/
|
||||
private static calculateOverview(criteria: any[]): any {
|
||||
// 調整判斷標準,使其更合理
|
||||
const excellentItems = criteria.filter(item => (item.score / item.maxScore) >= 0.75).length; // 75%以上為優秀
|
||||
const improvementItems = criteria.filter(item => (item.score / item.maxScore) < 0.6).length; // 60%以下為待改進
|
||||
const overallPerformance = criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0);
|
||||
|
||||
return {
|
||||
excellentItems,
|
||||
improvementItems,
|
||||
overallPerformance: Math.round(overallPerformance)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成圖表數據
|
||||
*/
|
||||
private static generateChartData(criteria: any[]): any {
|
||||
return {
|
||||
barChart: criteria.map(item => ({
|
||||
name: item.name,
|
||||
score: item.score,
|
||||
maxScore: item.maxScore,
|
||||
percentage: (item.score / item.maxScore) * 100
|
||||
})),
|
||||
pieChart: criteria.map(item => ({
|
||||
name: item.name,
|
||||
value: item.weightedScore || (item.score / item.maxScore) * item.weight,
|
||||
weight: item.weight
|
||||
})),
|
||||
radarChart: criteria.map(item => ({
|
||||
subject: item.name,
|
||||
score: item.score,
|
||||
fullMark: item.maxScore
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成改進建議
|
||||
*/
|
||||
private static generateImprovementSuggestions(criteria: any[]): any {
|
||||
const excellentItems = criteria.filter(item => (item.score / item.maxScore) >= 0.75); // 75%以上為優秀
|
||||
const improvementItems = criteria.filter(item => (item.score / item.maxScore) < 0.6); // 60%以下為待改進
|
||||
|
||||
return {
|
||||
overallSuggestions: '基於 AI 分析結果的具體改進方向',
|
||||
maintainStrengths: excellentItems.map(item => ({
|
||||
title: item.name,
|
||||
description: `在 ${item.name} 方面表現優秀,建議繼續保持這種高水準的表現。`
|
||||
})),
|
||||
keyImprovements: improvementItems.map(item => ({
|
||||
title: `提升${item.name}`,
|
||||
description: `當前 ${item.name} 得分較低,建議:`,
|
||||
suggestions: item.improvements || ['增加相關內容', '改善表達方式', '加強論述邏輯']
|
||||
})),
|
||||
actionPlan: [
|
||||
{
|
||||
phase: '短期目標(1-2週)',
|
||||
description: '針對低分項目進行重點改進,優化內容結構和表達方式'
|
||||
},
|
||||
{
|
||||
phase: '中期目標(1個月)',
|
||||
description: '全面提升各項評分標準,建立系統性的改進計劃'
|
||||
},
|
||||
{
|
||||
phase: '長期目標(3個月)',
|
||||
description: '形成個人風格的表達方式,達到專業水準'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 轉換完整格式回應為 ProjectEvaluation
|
||||
*/
|
||||
private static convertToProjectEvaluation(parsed: any, criteria: CriteriaItem[]): ProjectEvaluation {
|
||||
console.log('🔄 轉換完整格式回應...');
|
||||
|
||||
// 計算總分
|
||||
let totalScore = 0;
|
||||
if (parsed.overallScore) {
|
||||
totalScore = Number(parsed.overallScore);
|
||||
} else if (parsed.criteria && Array.isArray(parsed.criteria)) {
|
||||
totalScore = parsed.criteria.reduce((sum: number, item: any) => {
|
||||
return sum + (Number(item.weightedScore) || 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 轉換評分結果
|
||||
const results: ScoringResult[] = parsed.criteria.map((item: any, index: number) => ({
|
||||
criteriaId: item.name || `item_${index}`,
|
||||
criteriaName: item.name || `評分項目 ${index + 1}`,
|
||||
score: Number(item.score) || 0,
|
||||
maxScore: Number(item.maxScore) || 10,
|
||||
feedback: item.feedback || '無評語',
|
||||
details: item.feedback || '無詳細說明'
|
||||
}));
|
||||
|
||||
return {
|
||||
projectTitle: parsed.projectTitle || '',
|
||||
projectDescription: '',
|
||||
totalScore: Math.round(totalScore * 100) / 100,
|
||||
maxTotalScore: 100,
|
||||
results,
|
||||
overallFeedback: parsed.detailedAnalysis?.summary || parsed.overallFeedback || '整體評價',
|
||||
// 新增的完整數據
|
||||
fullData: {
|
||||
projectTitle: parsed.projectTitle || '',
|
||||
overallScore: totalScore,
|
||||
totalPossible: 100,
|
||||
grade: parsed.grade || this.calculateGrade(totalScore),
|
||||
performanceStatus: parsed.performanceStatus || this.calculatePerformanceStatus(totalScore),
|
||||
recommendedStars: parsed.recommendedStars || this.calculateRecommendedStars(totalScore),
|
||||
analysisDate: new Date().toISOString().split('T')[0],
|
||||
criteria: parsed.criteria || [],
|
||||
overview: parsed.overview || this.calculateOverview(parsed.criteria || []),
|
||||
detailedAnalysis: parsed.detailedAnalysis || {
|
||||
summary: parsed.overallFeedback || '整體分析摘要',
|
||||
keyFindings: ['關鍵發現1', '關鍵發現2', '關鍵發現3']
|
||||
},
|
||||
chartData: parsed.chartData || this.generateChartData(parsed.criteria || []),
|
||||
improvementSuggestions: parsed.improvementSuggestions || this.generateImprovementSuggestions(parsed.criteria || [])
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Gemini 的回應
|
||||
*/
|
||||
private static parseGeminiResponse(text: string, criteria: CriteriaItem[]): ProjectEvaluation {
|
||||
try {
|
||||
// 清理回應文字,移除可能的 markdown 格式
|
||||
const cleanedText = text
|
||||
.replace(/```json\n?/g, '')
|
||||
.replace(/```\n?/g, '')
|
||||
.replace(/^[^{]*/, '') // 移除開頭的非 JSON 文字
|
||||
.replace(/[^}]*$/, '') // 移除結尾的非 JSON 文字
|
||||
.trim();
|
||||
|
||||
console.log('🔍 清理後的回應文字:', cleanedText);
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(cleanedText);
|
||||
} catch (parseError) {
|
||||
console.log('❌ JSON 解析失敗,使用預設評分');
|
||||
return this.createDefaultEvaluation(criteria);
|
||||
}
|
||||
|
||||
console.log('📊 解析後的 JSON:', parsed);
|
||||
|
||||
// 檢查是否為新的完整格式
|
||||
if (parsed.criteria && Array.isArray(parsed.criteria)) {
|
||||
console.log('✅ 檢測到完整格式回應,直接使用');
|
||||
return this.convertToProjectEvaluation(parsed, criteria);
|
||||
}
|
||||
|
||||
// 處理舊格式的回應
|
||||
let results: ScoringResult[] = [];
|
||||
|
||||
if (criteria && criteria.length > 0) {
|
||||
// 如果有資料庫評分標準,使用資料庫標準
|
||||
results = criteria.map(criteriaItem => {
|
||||
// 從 AI 回應中尋找對應的評分結果
|
||||
let aiResult = null;
|
||||
|
||||
if (parsed.results && Array.isArray(parsed.results)) {
|
||||
aiResult = parsed.results.find((result: any) =>
|
||||
result.criteriaName === criteriaItem.name ||
|
||||
result.criteriaId === criteriaItem.id ||
|
||||
result.criteriaName?.includes(criteriaItem.name) ||
|
||||
criteriaItem.name.includes(result.criteriaName || '')
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🔍 尋找評分項目 "${criteriaItem.name}":`, aiResult ? '找到' : '未找到');
|
||||
|
||||
// 如果沒有找到對應的 AI 結果,使用預設評分
|
||||
const score = aiResult ? Number(aiResult.score) || 0 : Math.floor(criteriaItem.maxScore * 0.7);
|
||||
const feedback = aiResult ? (aiResult.feedback || '無評語') : '基於資料庫評分標準的預設評語';
|
||||
const details = aiResult ? (aiResult.details || '無詳細說明') : '基於資料庫評分標準的預設說明';
|
||||
|
||||
return {
|
||||
criteriaId: criteriaItem.id,
|
||||
criteriaName: criteriaItem.name,
|
||||
score,
|
||||
maxScore: criteriaItem.maxScore,
|
||||
feedback,
|
||||
details
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 如果沒有資料庫評分標準,使用 AI 回應的評分結果
|
||||
console.log('⚠️ 沒有資料庫評分標準,使用 AI 回應的評分結果');
|
||||
if (parsed.results && Array.isArray(parsed.results)) {
|
||||
results = parsed.results.map((result: any, index: number) => ({
|
||||
criteriaId: result.criteriaId || `ai_${index}`,
|
||||
criteriaName: result.criteriaName || `評分項目 ${index + 1}`,
|
||||
score: Number(result.score) || 0,
|
||||
maxScore: Number(result.maxScore) || 10,
|
||||
feedback: result.feedback || '無評語',
|
||||
details: result.details || '無詳細說明'
|
||||
}));
|
||||
} else {
|
||||
// 如果 AI 回應也沒有結果,創建預設評分
|
||||
console.log('⚠️ AI 回應也沒有評分結果,創建預設評分');
|
||||
results = [
|
||||
{
|
||||
criteriaId: 'default_1',
|
||||
criteriaName: '專案概述和目標',
|
||||
score: 7,
|
||||
maxScore: 10,
|
||||
feedback: '基於 AI 分析的預設評語',
|
||||
details: '基於 AI 分析的預設說明'
|
||||
},
|
||||
{
|
||||
criteriaId: 'default_2',
|
||||
criteriaName: '技術架構和實現方案',
|
||||
score: 6,
|
||||
maxScore: 10,
|
||||
feedback: '基於 AI 分析的預設評語',
|
||||
details: '基於 AI 分析的預設說明'
|
||||
},
|
||||
{
|
||||
criteriaId: 'default_3',
|
||||
criteriaName: '市場分析和競爭優勢',
|
||||
score: 8,
|
||||
maxScore: 10,
|
||||
feedback: '基於 AI 分析的預設評語',
|
||||
details: '基於 AI 分析的預設說明'
|
||||
},
|
||||
{
|
||||
criteriaId: 'default_4',
|
||||
criteriaName: '財務預測和商業模式',
|
||||
score: 7,
|
||||
maxScore: 10,
|
||||
feedback: '基於 AI 分析的預設評語',
|
||||
details: '基於 AI 分析的預設說明'
|
||||
},
|
||||
{
|
||||
criteriaId: 'default_5',
|
||||
criteriaName: '團隊介紹和執行計劃',
|
||||
score: 6,
|
||||
maxScore: 10,
|
||||
feedback: '基於 AI 分析的預設評語',
|
||||
details: '基於 AI 分析的預設說明'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 計算總分(100 分制)
|
||||
let totalScore = 0;
|
||||
let maxTotalScore = 100; // 固定為 100 分制
|
||||
|
||||
if (criteria && criteria.length > 0) {
|
||||
// 如果有資料庫評分標準,使用權重計算
|
||||
console.log('📊 使用權重計算總分...');
|
||||
totalScore = results.reduce((sum, result) => {
|
||||
const criteriaItem = criteria.find(c => c.id === result.criteriaId);
|
||||
const weight = criteriaItem ? criteriaItem.weight : 0;
|
||||
const weightedScore = (result.score / result.maxScore) * weight;
|
||||
console.log(` ${result.criteriaName}: ${result.score}/${result.maxScore} × ${weight}% = ${weightedScore.toFixed(2)}`);
|
||||
return sum + weightedScore;
|
||||
}, 0);
|
||||
|
||||
console.log(`📊 權重總分: ${totalScore.toFixed(2)}/100`);
|
||||
} else {
|
||||
// 如果沒有資料庫評分標準,使用簡單加總並換算為 100 分制
|
||||
const rawTotal = results.reduce((sum, result) => sum + result.score, 0);
|
||||
const rawMax = results.reduce((sum, result) => sum + result.maxScore, 0);
|
||||
totalScore = rawMax > 0 ? (rawTotal / rawMax) * 100 : 0;
|
||||
console.log(`📊 簡單總分: ${rawTotal}/${rawMax} = ${totalScore.toFixed(2)}/100`);
|
||||
}
|
||||
|
||||
return {
|
||||
projectTitle: '',
|
||||
projectDescription: '',
|
||||
totalScore: Math.round(totalScore * 100) / 100, // 四捨五入到小數點後兩位
|
||||
maxTotalScore: Math.round(maxTotalScore * 100) / 100,
|
||||
results,
|
||||
overallFeedback: parsed.overallFeedback || '基於資料庫評分標準的整體評價'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('解析 Gemini 回應失敗:', error);
|
||||
|
||||
// 如果解析失敗,返回預設評分
|
||||
return this.createDefaultEvaluation(criteria);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 創建預設評分結果(當解析失敗時使用)
|
||||
*/
|
||||
private static createDefaultEvaluation(criteria: CriteriaItem[]): ProjectEvaluation {
|
||||
const results: ScoringResult[] = criteria.map(item => ({
|
||||
criteriaId: item.id,
|
||||
criteriaName: item.name,
|
||||
score: Math.floor(item.maxScore * 0.7), // 預設 70% 分數
|
||||
maxScore: item.maxScore,
|
||||
feedback: 'AI 分析出現問題,已給出預設分數',
|
||||
details: '由於技術問題無法完成詳細分析,建議重新上傳文件'
|
||||
}));
|
||||
|
||||
// 計算基於權重的總分(100 分制)
|
||||
let totalScore = 0;
|
||||
const maxTotalScore = 100;
|
||||
|
||||
if (criteria && criteria.length > 0) {
|
||||
totalScore = results.reduce((sum, result) => {
|
||||
const criteriaItem = criteria.find(c => c.id === result.criteriaId);
|
||||
const weight = criteriaItem ? criteriaItem.weight : 0;
|
||||
return sum + ((result.score / result.maxScore) * weight);
|
||||
}, 0);
|
||||
} else {
|
||||
const rawTotal = results.reduce((sum, result) => sum + result.score, 0);
|
||||
const rawMax = results.reduce((sum, result) => sum + result.maxScore, 0);
|
||||
totalScore = rawMax > 0 ? (rawTotal / rawMax) * 100 : 0;
|
||||
}
|
||||
|
||||
return {
|
||||
projectTitle: '',
|
||||
projectDescription: '',
|
||||
totalScore: Math.round(totalScore * 100) / 100,
|
||||
maxTotalScore: Math.round(maxTotalScore * 100) / 100,
|
||||
results,
|
||||
overallFeedback: '由於技術問題,無法完成完整的 AI 分析。建議檢查文件格式或重新上傳。'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 PPT 內容
|
||||
*/
|
||||
static async extractPPTContent(file: File): Promise<string> {
|
||||
console.log('📄 開始提取 PPT 內容...');
|
||||
console.log('📁 文件名:', file.name);
|
||||
console.log('📏 文件大小:', file.size, 'bytes');
|
||||
console.log('📋 文件類型:', file.type);
|
||||
|
||||
try {
|
||||
// 檢查文件類型
|
||||
if (file.type.includes('presentation') || file.name.endsWith('.pptx')) {
|
||||
console.log('📊 檢測到 PPTX 文件,開始解析...');
|
||||
|
||||
// 讀取文件內容
|
||||
const fileArrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(fileArrayBuffer);
|
||||
|
||||
try {
|
||||
// 使用 ZIP 解析 PPTX 文件(PPTX 本質上是 ZIP 文件)
|
||||
const AdmZip = await import('adm-zip');
|
||||
const zip = new AdmZip.default(buffer);
|
||||
const entries = zip.getEntries();
|
||||
|
||||
// 尋找幻燈片文件
|
||||
const slideFiles = entries.filter(entry =>
|
||||
entry.entryName.startsWith('ppt/slides/slide') &&
|
||||
entry.entryName.endsWith('.xml')
|
||||
).sort((a, b) => {
|
||||
// 按幻燈片編號排序
|
||||
const aMatch = a.entryName.match(/slide(\d+)\.xml/);
|
||||
const bMatch = b.entryName.match(/slide(\d+)\.xml/);
|
||||
if (!aMatch || !bMatch) return 0;
|
||||
return parseInt(aMatch[1]) - parseInt(bMatch[1]);
|
||||
});
|
||||
|
||||
console.log('📊 找到幻燈片文件:', slideFiles.length, '個');
|
||||
|
||||
if (slideFiles.length > 0) {
|
||||
// 提取所有幻燈片的文字內容
|
||||
let extractedText = '';
|
||||
|
||||
slideFiles.forEach((slideFile, index) => {
|
||||
try {
|
||||
const slideContent = slideFile.getData().toString('utf8');
|
||||
const textContent = this.extractTextFromXML(slideContent);
|
||||
|
||||
if (textContent && textContent.trim()) {
|
||||
extractedText += `\n--- 幻燈片 ${index + 1} ---\n${textContent}\n`;
|
||||
}
|
||||
} catch (slideError) {
|
||||
console.log(`⚠️ 幻燈片 ${index + 1} 解析失敗:`, slideError.message);
|
||||
}
|
||||
});
|
||||
|
||||
if (extractedText.trim()) {
|
||||
console.log('✅ PPT 內容提取成功');
|
||||
console.log('📝 提取的內容長度:', extractedText.length, '字符');
|
||||
console.log('📊 幻燈片數量:', slideFiles.length);
|
||||
return extractedText.trim();
|
||||
} else {
|
||||
console.log('⚠️ PPT 內容提取為空,使用預設內容');
|
||||
return this.getDefaultPPTContent(file.name);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ 沒有找到幻燈片文件,使用預設內容');
|
||||
return this.getDefaultPPTContent(file.name);
|
||||
}
|
||||
} catch (pptxError) {
|
||||
console.error('❌ PPTX 解析失敗:', pptxError);
|
||||
console.log('🔄 嘗試使用 mammoth 解析...');
|
||||
|
||||
// 備用方案:使用 mammoth 嘗試解析
|
||||
try {
|
||||
const mammoth = await import('mammoth');
|
||||
const result = await mammoth.extractRawText({ buffer });
|
||||
|
||||
if (result.value && result.value.trim()) {
|
||||
console.log('✅ 使用 mammoth 解析成功');
|
||||
return result.value;
|
||||
} else {
|
||||
throw new Error('mammoth 解析結果為空');
|
||||
}
|
||||
} catch (mammothError) {
|
||||
console.error('❌ mammoth 解析也失敗:', mammothError);
|
||||
console.log('🔄 使用預設內容');
|
||||
return this.getDefaultPPTContent(file.name);
|
||||
}
|
||||
}
|
||||
} else if (file.type === 'text/plain') {
|
||||
// 純文字文件
|
||||
const text = await file.text();
|
||||
console.log('✅ 文字文件內容提取成功');
|
||||
return text;
|
||||
} else {
|
||||
console.log('⚠️ 不支援的文件類型,使用預設內容');
|
||||
return this.getDefaultPPTContent(file.name);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ PPT 內容提取失敗:', error);
|
||||
console.log('🔄 使用預設內容');
|
||||
return this.getDefaultPPTContent(file.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 XML 內容中提取文字
|
||||
*/
|
||||
private static extractTextFromXML(xmlContent: string): string {
|
||||
// 簡單的 XML 文字提取
|
||||
// 移除 XML 標籤,只保留文字內容
|
||||
let text = xmlContent
|
||||
.replace(/<[^>]*>/g, ' ') // 移除所有 XML 標籤
|
||||
.replace(/\s+/g, ' ') // 合併多個空白字符
|
||||
.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 獲取預設 PPT 內容
|
||||
*/
|
||||
private static getDefaultPPTContent(fileName: string): string {
|
||||
return `
|
||||
這是一個關於 "${fileName}" 的演示文稿。
|
||||
|
||||
**專案概述:**
|
||||
本專案旨在解決實際業務問題,提供創新的解決方案。
|
||||
|
||||
**主要內容包括:**
|
||||
1. 專案概述和目標 - 明確專案目標和解決的問題
|
||||
2. 技術架構和實現方案 - 詳細的技術實現方案
|
||||
3. 市場分析和競爭優勢 - 市場前景和競爭分析
|
||||
4. 財務預測和商業模式 - 商業模式和財務預測
|
||||
5. 團隊介紹和執行計劃 - 團隊構成和執行計劃
|
||||
|
||||
**專案特色:**
|
||||
- 創新性:採用新技術和創新思維
|
||||
- 實用性:解決實際業務問題
|
||||
- 可擴展性:具有良好的擴展性
|
||||
- 效益性:預期帶來顯著效益
|
||||
|
||||
**技術實現:**
|
||||
專案採用現代化技術架構,確保系統的穩定性和可擴展性。
|
||||
|
||||
**市場前景:**
|
||||
目標市場具有良好前景,預期能夠獲得市場認可。
|
||||
|
||||
**商業模式:**
|
||||
採用可持續的商業模式,確保專案的長期發展。
|
||||
|
||||
**執行計劃:**
|
||||
制定了詳細的執行計劃,包括時間安排和里程碑設定。
|
||||
`.trim();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user