diff --git a/app/api/evaluate/route.ts b/app/api/evaluate/route.ts new file mode 100644 index 0000000..c622d54 --- /dev/null +++ b/app/api/evaluate/route.ts @@ -0,0 +1,139 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { GeminiService } from '@/lib/services/gemini'; +import { CriteriaItemService } from '@/lib/services/database'; + +export async function POST(request: NextRequest) { + 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; + + console.log('🚀 開始處理評審請求...'); + console.log('📝 專案標題:', projectTitle); + console.log('📋 專案描述:', projectDescription); + console.log('📁 上傳文件:', file ? file.name : '無'); + console.log('🌐 網站連結:', websiteUrl || '無'); + + // 驗證必填欄位 + 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 criteria = templates[0].items || []; + console.log('📊 評分項目:', criteria); + console.log('✅ 評分標準載入完成,共', criteria.length, '個項目'); + + let content = ''; + + 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 (!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 } + ); + } + + // 提取內容 + content = await GeminiService.extractPPTContent(file); + } else if (websiteUrl) { + // 處理網站連結 + console.log('🌐 開始處理網站連結...'); + content = `網站內容分析: ${websiteUrl} + +由於目前是簡化版本,無法直接抓取網站內容。 +實際應用中應該使用網頁抓取技術來獲取網站內容進行分析。 + +專案描述: ${projectDescription || '無'}`; + } + + // 使用 Gemini AI 進行評分 + console.log('🤖 開始 AI 評分...'); + const evaluation = await GeminiService.analyzePresentation( + content, + projectTitle, + projectDescription || '', + criteria + ); + + // 儲存評審結果到資料庫(可選) + // TODO: 實作結果儲存功能 + + console.log('🎉 評審完成!'); + console.log('📊 最終評分結果:'); + console.log(' 總分:', evaluation.totalScore, '/', evaluation.maxTotalScore); + console.log(' 各項目評分:'); + evaluation.results.forEach(result => { + console.log(` - ${result.criteriaName}: ${result.score}/${result.maxScore}`); + }); + + return NextResponse.json({ + success: true, + data: { + evaluation, + projectTitle, + projectDescription, + fileInfo: file ? { + name: file.name, + size: file.size, + type: file.type + } : null, + websiteUrl: websiteUrl || null + } + }); + + } catch (error) { + console.error('❌ 評審處理失敗:', error); + return NextResponse.json( + { success: false, error: '評審處理失敗,請稍後再試' }, + { status: 500 } + ); + } +} diff --git a/app/results/page.tsx b/app/results/page.tsx index 5c78e20..89c2af7 100644 --- a/app/results/page.tsx +++ b/app/results/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { Sidebar } from "@/components/sidebar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -88,31 +88,105 @@ const mockResults = { ], } -const chartData = mockResults.criteria.map((item) => ({ - name: item.name, - score: item.score, - maxScore: item.maxScore, - percentage: (item.score / item.maxScore) * 100, -})) - -const pieData = mockResults.criteria.map((item) => ({ - name: item.name, - value: item.weightedScore, - weight: item.weight, -})) - -const radarData = mockResults.criteria.map((item) => ({ - subject: item.name, - score: item.score, - fullMark: item.maxScore, -})) +// 圖表數據將在組件內部動態生成 const COLORS = ["#0891b2", "#6366f1", "#f59e0b", "#dc2626", "#10b981"] export default function ResultsPage() { const [activeTab, setActiveTab] = useState("overview") + const [evaluationData, setEvaluationData] = useState(null) + const [isLoading, setIsLoading] = useState(true) const { toast } = useToast() + useEffect(() => { + // 從 localStorage 獲取評審結果 + const storedData = localStorage.getItem('evaluationResult') + if (storedData) { + try { + const data = JSON.parse(storedData) + setEvaluationData(data) + } catch (error) { + console.error('解析評審結果失敗:', error) + toast({ + title: "數據錯誤", + description: "無法載入評審結果,請重新進行評審", + variant: "destructive", + }) + } + } else { + toast({ + title: "無評審結果", + description: "請先進行評審以查看結果", + variant: "destructive", + }) + } + setIsLoading(false) + }, [toast]) + + // 如果正在載入,顯示載入狀態 + if (isLoading) { + return ( +
+ +
+
+
+
+
+

載入評審結果中...

+
+
+
+
+
+ ) + } + + // 如果沒有數據,顯示錯誤狀態 + if (!evaluationData) { + return ( +
+ +
+
+
+
+

沒有找到評審結果

+ +
+
+
+
+
+ ) + } + + // 使用真實數據或回退到模擬數據 + const results = evaluationData.evaluation?.fullData || mockResults + + // 確保所有必要的數據結構存在 + const safeResults = { + ...results, + overview: results.overview || { + excellentItems: 0, + improvementItems: 0, + overallPerformance: 0 + }, + chartData: results.chartData || { + barChart: [], + pieChart: [], + radarChart: [] + }, + improvementSuggestions: results.improvementSuggestions || { + overallSuggestions: '暫無改進建議', + maintainStrengths: [], + keyImprovements: [], + actionPlan: [] + } + } + const downloadReport = () => { toast({ title: "報告下載中", @@ -155,7 +229,7 @@ export default function ResultsPage() {

評審結果

- {mockResults.projectTitle} - 分析完成於 {mockResults.analysisDate} + {safeResults.projectTitle} - 分析完成於 {safeResults.analysisDate}

@@ -176,22 +250,24 @@ export default function ResultsPage() {
-
{mockResults.overallScore}
-
總分 / {mockResults.totalPossible}
+
{safeResults.overallScore}
+
總分 / {safeResults.totalPossible}
- {mockResults.grade} + {safeResults.grade}
等級評定
-
表現良好
+
{safeResults.performanceStatus}
- + {Array.from({ length: safeResults.recommendedStars || 0 }, (_, i) => ( + + ))}
推薦等級
@@ -217,7 +293,7 @@ export default function ResultsPage() {
- {mockResults.criteria.map((item, index) => ( + {safeResults.criteria.map((item, index) => (
@@ -243,21 +319,21 @@ export default function ResultsPage() { -
3
+
{safeResults.overview.excellentItems}
優秀項目
-
2
+
{safeResults.overview.improvementItems}
待改進項目
-
82%
+
{safeResults.overview.overallPerformance}%
整體表現
@@ -265,7 +341,7 @@ export default function ResultsPage() { - {mockResults.criteria.map((item, index) => ( + {safeResults.criteria.map((item, index) => (
@@ -322,7 +398,7 @@ export default function ResultsPage() { - + @@ -342,7 +418,7 @@ export default function ResultsPage() { - {pieData.map((entry, index) => ( + {safeResults.chartData.pieChart.map((entry, index) => ( ))} @@ -370,7 +446,7 @@ export default function ResultsPage() { - + @@ -385,86 +461,54 @@ export default function ResultsPage() { 整體改進建議 - 基於 AI 分析結果的具體改進方向 + {safeResults.improvementSuggestions.overallSuggestions}

繼續保持的優勢

-
-
邏輯結構清晰
-

- 內容組織有序,各部分銜接自然,建議在未來的作品中繼續保持這種清晰的邏輯架構。 -

-
-
-
內容品質優秀
-

- 資訊準確且豐富,專業度高,這是您的核心優勢,請繼續發揮。 -

-
+ {safeResults.improvementSuggestions.maintainStrengths.map((strength, index) => ( +
+
{strength.title}
+

+ {strength.description} +

+
+ ))}

重點改進方向

-
-
提升創新性
-

當前創新性得分較低,建議:

-
    -
  • • 嘗試從不同角度分析問題
  • -
  • • 引入新的理論或方法
  • -
  • • 提出獨特的解決方案
  • -
  • • 結合最新的行業趨勢
  • -
-
- -
-
優化視覺設計
-

視覺呈現有改進空間,建議:

-
    -
  • • 減少單頁文字密度
  • -
  • • 增加圖表和視覺元素
  • -
  • • 使用更多空白空間
  • -
  • • 統一字體和色彩風格
  • -
-
+ {safeResults.improvementSuggestions.keyImprovements.map((improvement, index) => ( +
+
{improvement.title}
+

{improvement.description}

+
    + {improvement.suggestions.map((suggestion, sIndex) => ( +
  • • {suggestion}
  • + ))} +
+
+ ))}

下一步行動計劃

-
-
- 1 + {safeResults.improvementSuggestions.actionPlan.map((action, index) => ( +
+
+ {index + 1} +
+
+
{action.phase}
+

{action.description}

+
-
-
短期目標(1-2週)
-

重新設計視覺版面,減少文字密度,增加圖表元素

-
-
-
-
- 2 -
-
-
中期目標(1個月)
-

- 研究行業最新趨勢,為內容增加創新元素和獨特觀點 -

-
-
-
-
- 3 -
-
-
長期目標(3個月)
-

建立個人風格的簡報模板,形成獨特的表達方式

-
-
+ ))}
diff --git a/app/upload/page.tsx b/app/upload/page.tsx index 22593c5..7faf7eb 100644 --- a/app/upload/page.tsx +++ b/app/upload/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useCallback } from "react" +import { useState, useCallback, useEffect } from "react" import { Sidebar } from "@/components/sidebar" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -22,6 +22,7 @@ interface UploadedFile { status: "uploading" | "completed" | "error" progress: number url?: string + file?: File // 儲存原始文件對象 } export default function UploadPage() { @@ -30,24 +31,52 @@ export default function UploadPage() { const [projectTitle, setProjectTitle] = useState("") const [projectDescription, setProjectDescription] = useState("") const [isAnalyzing, setIsAnalyzing] = useState(false) + const [analysisProgress, setAnalysisProgress] = useState(0) const { toast } = useToast() + // 動態進度條效果 + useEffect(() => { + let progressInterval: NodeJS.Timeout | null = null + + if (isAnalyzing) { + setAnalysisProgress(0) + + progressInterval = setInterval(() => { + setAnalysisProgress((prev) => { + if (prev >= 90) { + // 在90%時停止,等待實際完成 + return prev + } + // 模擬不規則的進度增長 + const increment = Math.random() * 8 + 2 // 2-10之間的隨機增量 + return Math.min(prev + increment, 90) + }) + }, 500) // 每500ms更新一次 + } else { + setAnalysisProgress(0) + } + + return () => { + if (progressInterval) { + clearInterval(progressInterval) + } + } + }, [isAnalyzing]) + const onDrop = useCallback((acceptedFiles: File[]) => { const newFiles: UploadedFile[] = acceptedFiles.map((file) => ({ id: Date.now().toString() + Math.random().toString(36).substr(2, 9), name: file.name, size: file.size, type: file.type, - status: "uploading", - progress: 0, + status: "completed", // 直接標記為完成,因為我們會在評審時處理文件 + progress: 100, + file: file, // 儲存原始文件對象 })) setFiles((prev) => [...prev, ...newFiles]) - - // 模擬上傳進度 - newFiles.forEach((file) => { - simulateUpload(file.id) - }) + + console.log('📁 文件已準備就緒:', newFiles.map(f => f.name).join(', ')) }, []) const { getRootProps, getInputProps, isDragActive } = useDropzone({ @@ -61,27 +90,6 @@ export default function UploadPage() { maxSize: 100 * 1024 * 1024, // 100MB }) - const simulateUpload = (fileId: string) => { - const interval = setInterval(() => { - setFiles((prev) => - prev.map((file) => { - if (file.id === fileId) { - const newProgress = Math.min(file.progress + Math.random() * 30, 100) - const status = newProgress === 100 ? "completed" : "uploading" - return { ...file, progress: newProgress, status } - } - return file - }), - ) - }, 500) - - setTimeout(() => { - clearInterval(interval) - setFiles((prev) => - prev.map((file) => (file.id === fileId ? { ...file, progress: 100, status: "completed" } : file)), - ) - }, 3000) - } const removeFile = (fileId: string) => { setFiles((prev) => prev.filter((file) => file.id !== fileId)) @@ -105,7 +113,7 @@ export default function UploadPage() { return } - const startAnalysis = () => { + const startAnalysis = async () => { if (files.length === 0 && !websiteUrl.trim()) { toast({ title: "請上傳文件或提供網站連結", @@ -126,16 +134,76 @@ export default function UploadPage() { setIsAnalyzing(true) - // 模擬分析過程 - setTimeout(() => { - setIsAnalyzing(false) - toast({ - title: "分析完成", - description: "評審結果已生成,請查看結果頁面", + try { + console.log('🚀 開始 AI 評審流程...') + console.log('📝 專案標題:', projectTitle) + console.log('📋 專案描述:', projectDescription) + console.log('📁 上傳文件數量:', files.length) + console.log('🌐 網站連結:', websiteUrl) + + // 準備表單數據 + const formData = new FormData() + formData.append('projectTitle', projectTitle) + formData.append('projectDescription', projectDescription) + + if (files.length > 0) { + // 只處理第一個文件(可以後續擴展支援多文件) + const firstFile = files[0] + if (firstFile.file) { + formData.append('file', firstFile.file) + console.log('📄 處理文件:', firstFile.name, '大小:', firstFile.size) + } else { + throw new Error('文件對象遺失,請重新上傳') + } + } + + if (websiteUrl.trim()) { + formData.append('websiteUrl', websiteUrl) + console.log('🌐 處理網站連結:', websiteUrl) + } + + // 發送評審請求 + console.log('📤 發送評審請求到 API...') + const response = await fetch('/api/evaluate', { + method: 'POST', + body: formData, }) - // 這裡會導向到結果頁面 - window.location.href = "/results" - }, 5000) + + const result = await response.json() + + if (result.success) { + console.log('✅ AI 評審完成!') + console.log('📊 評審結果:', result.data) + + // 設置進度為100% + setAnalysisProgress(100) + + // 等待一下讓用戶看到100%的進度 + await new Promise(resolve => setTimeout(resolve, 500)) + + toast({ + title: "評審完成", + description: `總分: ${result.data.evaluation.totalScore}/${result.data.evaluation.maxTotalScore}`, + }) + + // 儲存結果到 localStorage 以便結果頁面使用 + localStorage.setItem('evaluationResult', JSON.stringify(result.data)) + + // 導向到結果頁面 + window.location.href = "/results" + } else { + throw new Error(result.error || '評審失敗') + } + } catch (error) { + console.error('❌ AI 評審失敗:', error) + toast({ + title: "評審失敗", + description: error instanceof Error ? error.message : "請稍後再試", + variant: "destructive", + }) + } finally { + setIsAnalyzing(false) + } } return ( @@ -332,8 +400,11 @@ export default function UploadPage() { 正在分析內容... 預計 3-5 分鐘
- -

AI 正在深度分析您的內容,請稍候

+ +
+ AI 正在深度分析您的內容,請稍候 + {Math.round(analysisProgress)}% +
)}
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx index df83c10..c60262d 100644 --- a/components/ui/progress.tsx +++ b/components/ui/progress.tsx @@ -11,7 +11,7 @@ const Progress = React.forwardRef< >(({ className, value, ...props }, ref) => ( { + 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 { const sql = 'SELECT * FROM criteria_items WHERE template_id = ? ORDER BY sort_order'; const rows = await query(sql, [templateId]) as any[]; diff --git a/lib/services/gemini.ts b/lib/services/gemini.ts new file mode 100644 index 0000000..7793332 --- /dev/null +++ b/lib/services/gemini.ts @@ -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 { + 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 { + 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(); + } +} diff --git a/package.json b/package.json index b018977..96cfaaf 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "db:test": "node scripts/test-database.js" }, "dependencies": { + "@google/generative-ai": "^0.24.1", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "1.2.2", "@radix-ui/react-alert-dialog": "1.1.4", @@ -40,6 +41,7 @@ "@radix-ui/react-toggle-group": "1.1.1", "@radix-ui/react-tooltip": "1.1.6", "@vercel/analytics": "latest", + "adm-zip": "^0.5.16", "autoprefixer": "^10.4.20", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -49,10 +51,12 @@ "geist": "latest", "input-otp": "1.4.1", "lucide-react": "^0.454.0", + "mammoth": "^1.11.0", "mysql2": "^3.15.0", "next": "14.2.16", "next-themes": "^0.4.6", "node-fetch": "^3.3.2", + "node-pptx": "^1.0.0", "react": "^18", "react-day-picker": "9.8.0", "react-dom": "^18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bab07cf..a4dbb15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@google/generative-ai': + specifier: ^0.24.1 + version: 0.24.1 '@hookform/resolvers': specifier: ^3.10.0 version: 3.10.0(react-hook-form@7.60.0(react@18.0.0)) @@ -95,6 +98,9 @@ importers: '@vercel/analytics': specifier: latest version: 1.5.0(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0))(react@18.0.0) + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.0) @@ -122,6 +128,9 @@ importers: lucide-react: specifier: ^0.454.0 version: 0.454.0(react@18.0.0) + mammoth: + specifier: ^1.11.0 + version: 1.11.0 mysql2: specifier: ^3.15.0 version: 3.15.0 @@ -134,6 +143,9 @@ importers: node-fetch: specifier: ^3.3.2 version: 3.3.2 + node-pptx: + specifier: ^1.0.0 + version: 1.0.0 react: specifier: ^18 version: 18.0.0 @@ -224,6 +236,10 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@google/generative-ai@0.24.1': + resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==} + engines: {node: '>=18.0.0'} + '@hookform/resolvers@3.10.0': resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} peerDependencies: @@ -1357,6 +1373,20 @@ packages: vue-router: optional: true + '@xmldom/xmldom@0.8.11': + resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} + engines: {node: '>=10.0.0'} + + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -1376,10 +1406,19 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.8.6: resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} hasBin: true + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + browserslist@4.26.2: resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1412,6 +1451,9 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1459,6 +1501,10 @@ packages: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -1472,6 +1518,9 @@ packages: decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deferred@0.7.11: + resolution: {integrity: sha512-8eluCl/Blx4YOGwMapBvXRKxHXhA8ejDXYzEaK8+/gtcm8hRMhSLmXSqDmNUKNc/C8HNSmuyyp/hflhqDAvK2A==} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -1483,6 +1532,12 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dingbat-to-unicode@1.0.1: + resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} + + duck@0.1.12: + resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} + electron-to-chromium@1.5.222: resolution: {integrity: sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==} @@ -1503,16 +1558,40 @@ packages: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} + entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + es-toolkit@1.39.10: resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -1521,6 +1600,9 @@ packages: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -1528,6 +1610,16 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fs-extra@1.0.0: + resolution: {integrity: sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==} + + fs-promise@1.0.0: + resolution: {integrity: sha512-FXxDCsJ8iBVvoJtp1s5KhnK4jrPW5y5zd82ztrwQYx9a4goOb40rxrQYTndB/OOSl2LHNOyIb+jfmzuXNQM4jQ==} + deprecated: Use mz or fs-extra^3.0 with Promise Support + + fs@0.0.1-security: + resolution: {integrity: sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==} + geist@1.5.1: resolution: {integrity: sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ==} peerDependencies: @@ -1543,13 +1635,33 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hoek@4.3.1: + resolution: {integrity: sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw==} + engines: {node: '>=6.0.0'} + deprecated: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues. + + hoek@5.0.4: + resolution: {integrity: sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==} + engines: {node: '>=8.9.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + + hoek@6.1.3: + resolution: {integrity: sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==} + deprecated: This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues. + iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} engines: {node: '>=0.10.0'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@10.1.3: resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + input-otp@1.4.1: resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} peerDependencies: @@ -1563,13 +1675,37 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isemail@3.2.0: + resolution: {integrity: sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==} + engines: {node: '>=4.0.0'} + jiti@2.5.1: resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} hasBin: true + joi@13.7.0: + resolution: {integrity: sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==} + engines: {node: '>=8.9.0'} + deprecated: This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial). + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsonfile@2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + klaw@1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -1641,6 +1777,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lop@0.4.2: + resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==} + lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -1657,6 +1796,11 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + mammoth@1.11.0: + resolution: {integrity: sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==} + engines: {node: '>=12.0.0'} + hasBin: true + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1674,10 +1818,16 @@ packages: resolution: {integrity: sha512-tT6pomf5Z/I7Jzxu8sScgrYBMK9bUFWd7Kbo6Fs1L0M13OOIJ/ZobGKS3Z7tQ8Re4lj+LnLXIQVZZxa3fhYKzA==} engines: {node: '>= 8.0'} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.3: resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} engines: {node: '>=12.0.0'} + nan@2.23.0: + resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1689,6 +1839,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + next@14.2.16: resolution: {integrity: sha512-LcO7WnFu6lYSvCzZoo1dB+IO0xXz5uEv52HF1IUN0IqVTUIZGHuuR10I5efiLadGt+4oZqTcNZyVVEem/TM5nA==} engines: {node: '>=18.17.0'} @@ -1712,10 +1865,16 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-expat@2.4.1: + resolution: {integrity: sha512-uWgvQLgo883NKIL+66oJsK9ysKK3ej0YjVCPBZzO/7wMAuH68/Yb7+JwPWNaVq0yPaxrb48AoEXfYEc8gsmFbg==} + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-pptx@1.0.0: + resolution: {integrity: sha512-57IaqFfyH7gu/YyLRX1dUtIvOXqpxkG5IUqCdG86JX3LpovPW9k7WtVWJjoNcaxwkquL35JlDr4eT4UZxYlfRQ==} + node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -1727,6 +1886,16 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + option@0.2.4: + resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1741,9 +1910,19 @@ packages: resolution: {integrity: sha512-27VKOqrYfPncKA2NrFOVhP5MGAfHKLYn/Q0mz9cNQyRAKYi3VNHwYU2qKKqPCqgBmeeJ0uAFB56NumXZ5ZReXg==} engines: {node: ^10 || ^12 || >=14} + ppt-template@1.0.9: + resolution: {integrity: sha512-CNqOBj3oI6AwClm4rpi4ewPY2jVuI1hsEdOue1XutRwxfpvEv8Fdk8fT0RayJmRzkUL6D1PWfBiRs9ptSBhWsA==} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + react-day-picker@9.8.0: resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==} engines: {node: '>=18'} @@ -1822,6 +2001,9 @@ packages: resolution: {integrity: sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + recharts@3.2.1: resolution: {integrity: sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==} engines: {node: '>=18'} @@ -1841,15 +2023,24 @@ packages: reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + scheduler@0.21.0: resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -1860,6 +2051,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} @@ -1868,6 +2062,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -1900,20 +2097,41 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + timers-ext@0.1.8: + resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} + engines: {node: '>=0.12'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + topo@3.0.3: + resolution: {integrity: sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==} + deprecated: This module has moved and is now available at @hapi/topo. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues. + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tw-animate-css@1.3.3: resolution: {integrity: sha512-tXE2TRWrskc4TU3RDd7T8n8Np/wCfoeH9gz22c7PzYqNPQ9FBGFbWWzwL0JyHcFp+jHozmF76tbHfPAx22ua2Q==} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typescript@5.0.2: resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==} engines: {node: '>=12.20'} hasBin: true + underscore@1.13.7: + resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} + undici-types@6.11.1: resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} @@ -1948,6 +2166,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vaul@0.9.9: resolution: {integrity: sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==} peerDependencies: @@ -1961,6 +2182,22 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + xml2js@0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + + xml2json@0.12.0: + resolution: {integrity: sha512-EPJHRWJnJUYbJlzR4pBhZODwWdi2IaYGtDdteJi0JpZ4OD31IplWALuit8r73dJuM4iHZdDVKY1tLqY2UICejg==} + hasBin: true + + xmlbuilder@10.1.1: + resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} + engines: {node: '>=4.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -1996,6 +2233,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@google/generative-ai@0.24.1': {} + '@hookform/resolvers@3.10.0(react-hook-form@7.60.0(react@18.0.0))': dependencies: react-hook-form: 7.60.0(react@18.0.0) @@ -3064,6 +3303,16 @@ snapshots: next: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0) react: 18.0.0 + '@xmldom/xmldom@0.8.11': {} + + adm-zip@0.5.16: {} + + any-promise@1.3.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -3082,8 +3331,16 @@ snapshots: aws-ssl-profiles@1.1.2: {} + base64-js@1.5.1: {} + baseline-browser-mapping@2.8.6: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bluebird@3.4.7: {} + browserslist@4.26.2: dependencies: baseline-browser-mapping: 2.8.6 @@ -3120,6 +3377,8 @@ snapshots: - '@types/react' - '@types/react-dom' + core-util-is@1.0.3: {} + csstype@3.1.3: {} d3-array@3.2.4: @@ -3160,6 +3419,11 @@ snapshots: d3-timer@3.0.1: {} + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + data-uri-to-buffer@4.0.1: {} date-fns-jalali@4.1.0-0: {} @@ -3168,12 +3432,26 @@ snapshots: decimal.js-light@2.5.1: {} + deferred@0.7.11: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + next-tick: 1.1.0 + timers-ext: 0.1.8 + denque@2.1.0: {} detect-libc@2.1.0: {} detect-node-es@1.1.0: {} + dingbat-to-unicode@1.0.1: {} + + duck@0.1.12: + dependencies: + underscore: 1.13.7 + electron-to-chromium@1.5.222: {} embla-carousel-react@8.5.1(react@18.0.0): @@ -3193,12 +3471,48 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.3 + entities@1.1.2: {} + es-toolkit@1.39.10: {} + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + escalade@3.2.0: {} + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + eventemitter3@5.0.1: {} + ext@1.7.0: + dependencies: + type: 2.7.3 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -3208,12 +3522,29 @@ snapshots: dependencies: tslib: 2.8.1 + file-uri-to-path@1.0.0: {} + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 fraction.js@4.3.7: {} + fs-extra@1.0.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 2.4.0 + klaw: 1.3.1 + + fs-promise@1.0.0: + dependencies: + any-promise: 1.3.0 + fs-extra: 1.0.0 + mz: 2.7.0 + thenify-all: 1.6.0 + + fs@0.0.1-security: {} + geist@1.5.1(next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0)): dependencies: next: 14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -3226,12 +3557,22 @@ snapshots: graceful-fs@4.2.11: {} + hoek@4.3.1: {} + + hoek@5.0.4: {} + + hoek@6.1.3: {} + iconv-lite@0.7.0: dependencies: safer-buffer: 2.1.2 + immediate@3.0.6: {} + immer@10.1.3: {} + inherits@2.0.4: {} + input-otp@1.4.1(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: react: 18.0.0 @@ -3241,10 +3582,41 @@ snapshots: is-property@1.0.2: {} + isarray@1.0.0: {} + + isemail@3.2.0: + dependencies: + punycode: 2.3.1 + jiti@2.5.1: {} + joi@13.7.0: + dependencies: + hoek: 5.0.4 + isemail: 3.2.0 + topo: 3.0.3 + js-tokens@4.0.0: {} + jsonfile@2.4.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + klaw@1.3.1: + optionalDependencies: + graceful-fs: 4.2.11 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-darwin-arm64@1.30.1: optional: true @@ -3296,6 +3668,12 @@ snapshots: dependencies: js-tokens: 4.0.0 + lop@0.4.2: + dependencies: + duck: 0.1.12 + option: 0.2.4 + underscore: 1.13.7 + lru-cache@7.18.3: {} lru.min@1.1.2: {} @@ -3308,6 +3686,19 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mammoth@1.11.0: + dependencies: + '@xmldom/xmldom': 0.8.11 + argparse: 1.0.10 + base64-js: 1.5.1 + bluebird: 3.4.7 + dingbat-to-unicode: 1.0.1 + jszip: 3.10.1 + lop: 0.4.2 + path-is-absolute: 1.0.1 + underscore: 1.13.7 + xmlbuilder: 10.1.1 + minipass@7.1.2: {} minizlib@3.0.2: @@ -3328,10 +3719,18 @@ snapshots: seq-queue: 0.0.5 sqlstring: 2.3.3 + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + named-placeholders@1.1.3: dependencies: lru-cache: 7.18.3 + nan@2.23.0: {} + nanoid@3.3.11: {} next-themes@0.4.6(react-dom@18.0.0(react@18.0.0))(react@18.0.0): @@ -3339,6 +3738,8 @@ snapshots: react: 18.0.0 react-dom: 18.0.0(react@18.0.0) + next-tick@1.1.0: {} + next@14.2.16(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: '@next/env': 14.2.16 @@ -3366,18 +3767,36 @@ snapshots: node-domexception@1.0.0: {} + node-expat@2.4.1: + dependencies: + bindings: 1.5.0 + nan: 2.23.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-pptx@1.0.0: + dependencies: + deferred: 0.7.11 + fs: 0.0.1-security + ppt-template: 1.0.9 + xml2json: 0.12.0 + node-releases@2.0.21: {} normalize-range@0.1.2: {} object-assign@4.1.1: {} + option@0.2.4: {} + + pako@1.0.11: {} + + path-is-absolute@1.0.1: {} + picocolors@1.1.1: {} postcss-value-parser@4.2.0: {} @@ -3394,12 +3813,23 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + ppt-template@1.0.9: + dependencies: + entities: 1.1.2 + fs-promise: 1.0.0 + jszip: 3.10.1 + xml2js: 0.4.23 + + process-nextick-args@2.0.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 + punycode@2.3.1: {} + react-day-picker@9.8.0(react@18.0.0): dependencies: '@date-fns/tz': 1.2.0 @@ -3471,6 +3901,16 @@ snapshots: dependencies: loose-envify: 1.4.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + recharts@3.2.1(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react-is@16.13.1)(react@18.0.0)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@18.0.0)(react@18.0.0)(redux@5.0.1))(react@18.0.0) @@ -3499,14 +3939,20 @@ snapshots: reselect@5.1.1: {} + safe-buffer@5.1.2: {} + safer-buffer@2.1.2: {} + sax@1.4.1: {} + scheduler@0.21.0: dependencies: loose-envify: 1.4.0 seq-queue@0.0.5: {} + setimmediate@1.0.5: {} + sonner@1.7.4(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: react: 18.0.0 @@ -3514,10 +3960,16 @@ snapshots: source-map-js@1.2.1: {} + sprintf-js@1.0.3: {} + sqlstring@2.3.3: {} streamsearch@1.1.0: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + styled-jsx@5.1.1(react@18.0.0): dependencies: client-only: 0.0.1 @@ -3542,14 +3994,35 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + timers-ext@0.1.8: + dependencies: + es5-ext: 0.10.64 + next-tick: 1.1.0 + tiny-invariant@1.3.3: {} + topo@3.0.3: + dependencies: + hoek: 6.1.3 + tslib@2.8.1: {} tw-animate-css@1.3.3: {} + type@2.7.3: {} + typescript@5.0.2: {} + underscore@1.13.7: {} + undici-types@6.11.1: {} update-browserslist-db@1.1.3(browserslist@4.26.2): @@ -3577,6 +4050,8 @@ snapshots: dependencies: react: 18.0.0 + util-deprecate@1.0.2: {} + vaul@0.9.9(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0): dependencies: '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.0.0)(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -3605,6 +4080,21 @@ snapshots: web-streams-polyfill@3.3.3: {} + xml2js@0.4.23: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xml2json@0.12.0: + dependencies: + hoek: 4.3.1 + joi: 13.7.0 + node-expat: 2.4.1 + + xmlbuilder@10.1.1: {} + + xmlbuilder@11.0.1: {} + yallist@5.0.0: {} zod@3.25.67: {} diff --git a/scripts/test-criteria-api.js b/scripts/test-criteria-api.js deleted file mode 100644 index 38b64e9..0000000 --- a/scripts/test-criteria-api.js +++ /dev/null @@ -1,69 +0,0 @@ -const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); - -const API_BASE = 'http://localhost:3000/api'; - -async function testCriteriaAPI() { - try { - console.log('🔄 測試評分標準 API...'); - - // 1. 測試獲取預設模板 - console.log('\n1. 測試獲取預設模板...'); - const defaultResponse = await fetch(`${API_BASE}/criteria-templates/default`); - const defaultData = await defaultResponse.json(); - - if (defaultData.success) { - console.log('✅ 預設模板獲取成功'); - console.log(` 模板名稱: ${defaultData.data.name}`); - console.log(` 評分項目數量: ${defaultData.data.items.length}`); - defaultData.data.items.forEach((item, index) => { - console.log(` ${index + 1}. ${item.name} (權重: ${item.weight}%)`); - }); - } else { - console.log('❌ 預設模板獲取失敗:', defaultData.error); - } - - // 2. 測試創建新模板 - console.log('\n2. 測試創建新模板...'); - const newTemplate = { - name: '測試模板', - description: '這是一個測試模板', - items: [ - { name: '測試項目1', description: '測試描述1', weight: 50, maxScore: 10 }, - { name: '測試項目2', description: '測試描述2', weight: 50, maxScore: 10 } - ] - }; - - const createResponse = await fetch(`${API_BASE}/criteria-templates`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(newTemplate) - }); - const createData = await createResponse.json(); - - if (createData.success) { - console.log('✅ 新模板創建成功'); - console.log(` 模板 ID: ${createData.data.id}`); - } else { - console.log('❌ 新模板創建失敗:', createData.error); - } - - // 3. 測試獲取所有模板 - console.log('\n3. 測試獲取所有模板...'); - const allResponse = await fetch(`${API_BASE}/criteria-templates`); - const allData = await allResponse.json(); - - if (allData.success) { - console.log('✅ 所有模板獲取成功'); - console.log(` 模板總數: ${allData.data.length}`); - } else { - console.log('❌ 所有模板獲取失敗:', allData.error); - } - - console.log('\n🎉 API 測試完成!'); - - } catch (error) { - console.error('❌ 測試失敗:', error.message); - } -} - -testCriteriaAPI(); diff --git a/scripts/test-database.js b/scripts/test-database.js deleted file mode 100644 index 8bcd6f2..0000000 --- a/scripts/test-database.js +++ /dev/null @@ -1,54 +0,0 @@ -const mysql = require('mysql2/promise'); - -// 資料庫配置 -const dbConfig = { - host: process.env.DB_HOST || 'mysql.theaken.com', - port: parseInt(process.env.DB_PORT || '33306'), - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || 'zh6161168', - database: process.env.DB_NAME || 'db_AI_scoring', - charset: 'utf8mb4', - timezone: '+08:00', -}; - -async function testDatabase() { - let connection; - - try { - console.log('🔄 正在測試資料庫連接...'); - - connection = await mysql.createConnection(dbConfig); - - // 測試基本連接 - await connection.ping(); - console.log('✅ 資料庫連接成功'); - - // 測試查詢 - const [rows] = await connection.query('SELECT COUNT(*) as count FROM criteria_templates'); - console.log(`✅ 找到 ${rows[0].count} 個評分標準模板`); - - // 顯示所有資料表 - const [tables] = await connection.query('SHOW TABLES'); - console.log('📊 資料庫中的資料表:'); - tables.forEach(table => { - console.log(` - ${Object.values(table)[0]}`); - }); - - // 測試預設數據 - const [criteriaItems] = await connection.query('SELECT * FROM criteria_items ORDER BY sort_order'); - console.log('📋 預設評分項目:'); - criteriaItems.forEach(item => { - console.log(` - ${item.name} (權重: ${item.weight}%, 滿分: ${item.max_score})`); - }); - - await connection.end(); - console.log('🎉 資料庫測試完成!'); - - } catch (error) { - console.error('❌ 資料庫測試失敗:', error.message); - console.error('詳細錯誤:', error); - process.exit(1); - } -} - -testDatabase(); diff --git a/scripts/test-single-template.js b/scripts/test-single-template.js deleted file mode 100644 index 4ce5bae..0000000 --- a/scripts/test-single-template.js +++ /dev/null @@ -1,79 +0,0 @@ -const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); - -const API_BASE = 'http://localhost:3000/api'; - -async function testSingleTemplateMode() { - try { - console.log('🔄 測試單一模板模式...'); - - // 1. 測試獲取現有模板 - console.log('\n1. 測試獲取現有模板...'); - const getResponse = await fetch(`${API_BASE}/criteria-templates`); - const getData = await getResponse.json(); - - if (getData.success) { - console.log(`✅ 找到 ${getData.data.length} 個模板`); - if (getData.data.length > 0) { - const template = getData.data[0]; - console.log(` 模板名稱: ${template.name}`); - console.log(` 評分項目數量: ${template.items.length}`); - } - } else { - console.log('❌ 獲取模板失敗:', getData.error); - } - - // 2. 測試覆蓋模板 - console.log('\n2. 測試覆蓋模板...'); - const templateData = { - name: '我的評分標準', - description: '這是我的自定義評分標準', - items: [ - { name: '內容品質', description: '內容的準確性和完整性', weight: 30, maxScore: 10 }, - { name: '視覺設計', description: '版面設計和視覺效果', weight: 25, maxScore: 10 }, - { name: '邏輯結構', description: '內容組織的邏輯性', weight: 25, maxScore: 10 }, - { name: '創新性', description: '創意思維的展現', weight: 20, maxScore: 10 } - ] - }; - - const saveResponse = await fetch(`${API_BASE}/criteria-templates`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(templateData) - }); - const saveData = await saveResponse.json(); - - if (saveData.success) { - console.log('✅ 模板覆蓋成功'); - console.log(` 模板 ID: ${saveData.data.id}`); - } else { - console.log('❌ 模板覆蓋失敗:', saveData.error); - } - - // 3. 驗證覆蓋結果 - console.log('\n3. 驗證覆蓋結果...'); - const verifyResponse = await fetch(`${API_BASE}/criteria-templates`); - const verifyData = await verifyResponse.json(); - - if (verifyData.success) { - console.log(`✅ 驗證成功,現在有 ${verifyData.data.length} 個模板`); - if (verifyData.data.length > 0) { - const template = verifyData.data[0]; - console.log(` 模板名稱: ${template.name}`); - console.log(` 模板描述: ${template.description}`); - console.log(` 評分項目數量: ${template.items.length}`); - template.items.forEach((item, index) => { - console.log(` ${index + 1}. ${item.name} (權重: ${item.weight}%)`); - }); - } - } else { - console.log('❌ 驗證失敗:', verifyData.error); - } - - console.log('\n🎉 單一模板模式測試完成!'); - - } catch (error) { - console.error('❌ 測試失敗:', error.message); - } -} - -testSingleTemplateMode(); diff --git a/scripts/test-weight-display.js b/scripts/test-weight-display.js deleted file mode 100644 index 3aa105e..0000000 --- a/scripts/test-weight-display.js +++ /dev/null @@ -1,23 +0,0 @@ -// 測試權重顯示格式 -const testWeights = [25.00, 20.00, 20.00, 15.00, 20.00]; - -console.log('🔄 測試權重顯示格式...'); - -// 計算總權重 -const totalWeight = testWeights.reduce((sum, weight) => sum + weight, 0); - -console.log('個別權重:'); -testWeights.forEach((weight, index) => { - console.log(` ${index + 1}. ${weight.toFixed(1)}%`); -}); - -console.log(`\n總權重: ${totalWeight.toFixed(1)}%`); -console.log(`是否等於 100%: ${totalWeight === 100 ? '✅' : '❌'}`); - -// 測試權重顯示的各種格式 -console.log('\n權重顯示格式測試:'); -console.log(`原始格式: ${totalWeight}%`); -console.log(`toFixed(1): ${totalWeight.toFixed(1)}%`); -console.log(`toFixed(0): ${totalWeight.toFixed(0)}%`); - -console.log('\n🎉 權重顯示格式測試完成!'); diff --git a/scripts/test-weight-fix.js b/scripts/test-weight-fix.js deleted file mode 100644 index 22de81e..0000000 --- a/scripts/test-weight-fix.js +++ /dev/null @@ -1,41 +0,0 @@ -// 測試權重修復 -console.log('🔄 測試權重修復...'); - -// 模擬可能出現的權重數據類型 -const testCases = [ - { weight: 25.00 }, // 正常數字 - { weight: "25.00" }, // 字符串數字 - { weight: "25" }, // 字符串整數 - { weight: null }, // null 值 - { weight: undefined }, // undefined 值 - { weight: "" }, // 空字符串 - { weight: "abc" }, // 非數字字符串 -]; - -console.log('\n測試各種權重數據類型:'); -testCases.forEach((item, index) => { - const originalWeight = item.weight; - const safeWeight = Number(item.weight) || 0; - const formattedWeight = safeWeight.toFixed(1); - - console.log(`${index + 1}. 原始: ${originalWeight} (${typeof originalWeight})`); - console.log(` 安全轉換: ${safeWeight} (${typeof safeWeight})`); - console.log(` 格式化: ${formattedWeight}%`); - console.log(''); -}); - -// 測試總權重計算 -console.log('測試總權重計算:'); -const criteria = [ - { weight: 25.00 }, - { weight: "20.00" }, - { weight: 20 }, - { weight: "15.00" }, - { weight: null }, -]; - -const totalWeight = criteria.reduce((sum, item) => sum + (Number(item.weight) || 0), 0); -console.log(`總權重: ${totalWeight} (${typeof totalWeight})`); -console.log(`格式化總權重: ${Number(totalWeight).toFixed(1)}%`); - -console.log('\n🎉 權重修復測試完成!');