798 lines
27 KiB
TypeScript
798 lines
27 KiB
TypeScript
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 到滿分的分數,要敢於給出極高分(9-10分)和極低分(1-3分)
|
||
2. 為每個項目提供具體的評分理由、優點和改進建議
|
||
3. 計算總分(各項目分數 × 權重比例)
|
||
4. 提供整體評價和建議
|
||
5. 分析優秀項目和待改進項目的數量
|
||
6. 給出等級評比 (S、A+、A、A-、B+、B、B-、C、D)
|
||
7. 給出表現狀態 (表現極優、表現良好、表現普通、表現有待加強)
|
||
8. 給出推薦星星數量 (1-5顆星)
|
||
9. **重要:請根據實際內容質量給出真實評分,不要過於保守,優秀的內容應該得到高分,糟糕的內容應該得到低分**
|
||
|
||
**回應格式 (請嚴格按照以下 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;
|
||
}
|
||
|
||
/**
|
||
* 計算總覽統計 - 基於 criteria_items 的平均分作為閾值
|
||
*/
|
||
private static calculateOverview(criteria: any[]): any {
|
||
if (!criteria || criteria.length === 0) {
|
||
return {
|
||
excellentItems: 0,
|
||
improvementItems: 0,
|
||
overallPerformance: 0
|
||
};
|
||
}
|
||
|
||
// 計算所有項目的平均分數(不考慮權重)
|
||
const totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
||
const averageScore = totalScore / criteria.length;
|
||
|
||
console.log('🔍 計算 overview 統計:');
|
||
console.log(' 評分項目:', criteria.map(item => `${item.name}: ${item.score}/${item.maxScore}`));
|
||
console.log(' 總分:', totalScore);
|
||
console.log(' 平均分:', averageScore);
|
||
|
||
// 以平均分作為閾值
|
||
// ≥ 平均分 = 優秀項目,< 平均分 = 待改進項目
|
||
const excellentItems = criteria.filter(item => item.score >= averageScore).length;
|
||
const improvementItems = criteria.filter(item => item.score < averageScore).length;
|
||
|
||
console.log(' 優秀項目 (≥' + averageScore + '):', excellentItems);
|
||
console.log(' 待改進項目 (<' + averageScore + '):', improvementItems);
|
||
|
||
// 整體表現:基於權重的加權平均分數
|
||
const overallPerformance = Math.round(criteria.reduce((sum, item) => sum + (item.score / item.maxScore) * item.weight, 0));
|
||
|
||
return {
|
||
excellentItems,
|
||
improvementItems,
|
||
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 totalScore = criteria.reduce((sum, item) => sum + item.score, 0);
|
||
const averageScore = totalScore / criteria.length;
|
||
|
||
const excellentItems = criteria.filter(item => item.score >= averageScore); // ≥ 平均分為優秀
|
||
const improvementItems = criteria.filter(item => item.score < averageScore); // < 平均分為待改進
|
||
|
||
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: this.calculateOverview(parsed.criteria || []),
|
||
detailedAnalysis: parsed.detailedAnalysis || {
|
||
summary: parsed.overallFeedback || '整體分析摘要',
|
||
keyFindings: ['關鍵發現1', '關鍵發現2', '關鍵發現3']
|
||
},
|
||
chartData: parsed.chartData || this.generateChartData(parsed.criteria || []),
|
||
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();
|
||
}
|
||
}
|