Initial commit: 5 Why Root Cause Analyzer v1.0.0

Phase 0 & Phase 2 completed:
- Project structure setup
- Environment configuration (.env, .gitignore)
- Enterprise-grade dependencies (bcrypt, helmet, mysql2, etc.)
- Complete database schema with 8 tables + 2 views
- Database initialization scripts
- Comprehensive documentation

Database Tables:
- users (user management with 3-tier permissions)
- analyses (analysis records)
- analysis_perspectives (multi-angle analysis)
- analysis_whys (detailed 5 Why records)
- llm_configs (LLM API configurations)
- system_settings (system parameters)
- audit_logs (security audit trail)
- sessions (session management)

Tech Stack:
- Backend: Node.js + Express
- Frontend: React 18 + Vite + Tailwind CSS
- Database: MySQL 9.4.0
- AI: Ollama API (qwen2.5:3b)

Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
donald
2025-12-05 18:29:29 +08:00
commit 78efac64e2
23 changed files with 3059 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(npm install)",
"Bash(npm run db:test:*)",
"Bash(npm run db:init:*)",
"Bash(node scripts/init-database-simple.js:*)",
"Bash(git init:*)",
"Bash(git config:*)",
"Bash(git add:*)"
],
"deny": [],
"ask": []
}
}

35
.env.example Normal file
View File

@@ -0,0 +1,35 @@
# Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_NAME=5why_analyzer
# Server Configuration
SERVER_HOST=localhost
SERVER_PORT=3001
CLIENT_PORT=5173
# Ollama API Configuration
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
OLLAMA_MODEL=qwen2.5:3b
# LLM API Keys (Optional - for admin configuration)
GEMINI_API_KEY=
DEEPSEEK_API_KEY=
OPENAI_API_KEY=
# Session Secret (Generate a random string)
SESSION_SECRET=your-secret-key-here
# Admin Configuration
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD_HASH=
# Security
BCRYPT_ROUNDS=10
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW_MS=900000
# Environment
NODE_ENV=development

79
.gitignore vendored Normal file
View File

@@ -0,0 +1,79 @@
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Dependencies
node_modules/
jspm_packages/
package-lock.json
yarn.lock
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist/
build/
.next/
out/
# Testing
coverage/
.nyc_output/
*.lcov
# Temporary files
*.tmp
*.temp
.cache/
# Database
*.sql
*.sqlite
*.db
# Sensitive files
security_audit.md
*.key
*.pem
*.cert

544
5why-analyzer (1).jsx Normal file
View File

@@ -0,0 +1,544 @@
import { useState } from "react";
export default function FiveWhyAnalyzer() {
const [finding, setFinding] = useState("");
const [jobContent, setJobContent] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [translating, setTranslating] = useState(false);
const [error, setError] = useState("");
const [outputLanguage, setOutputLanguage] = useState("zh-TW");
const [currentLanguage, setCurrentLanguage] = useState("zh-TW");
const [showGuide, setShowGuide] = useState(false);
const languages = [
{ code: "zh-TW", name: "繁體中文", flag: "🇹🇼" },
{ code: "zh-CN", name: "简体中文", flag: "🇨🇳" },
{ code: "en", name: "English", flag: "🇺🇸" },
{ code: "ja", name: "日本語", flag: "🇯🇵" },
{ code: "ko", name: "한국어", flag: "🇰🇷" },
{ code: "vi", name: "Tiếng Việt", flag: "🇻🇳" },
{ code: "th", name: "ภาษาไทย", flag: "🇹🇭" },
];
const guidelines = [
{
title: "精準定義問題",
subtitle: "描述現象,而非結論",
icon: "🎯",
color: "bg-rose-50 border-rose-200",
content: "起點若是錯誤後續分析皆是枉然。必須客觀描述「發生了什麼事」包含人、事、時、地、物5W1H。",
example: { bad: "機器壞了", good: "A 機台在下午 2 點運轉時,主軸過熱導致停機" }
},
{
title: "聚焦流程與系統",
subtitle: "而非責備個人",
icon: "⚙️",
color: "bg-amber-50 border-amber-200",
content: "若分析導向「某人不小心」或「某人忘記了」,這不是根本原因。人本來就會犯錯,應追問:「為什麼系統允許這個疏失發生?」",
principle: "解決問題的機制,而非責備犯錯的人"
},
{
title: "基於事實與現場",
subtitle: "拒絕猜測",
icon: "🔍",
color: "bg-emerald-50 border-emerald-200",
content: "每一個「為什麼」的回答都必須是經過查證的事實,不能是「我覺得應該是...」或「可能是...」。",
principle: "三現主義:現場、現物、現實"
},
{
title: "邏輯雙向檢核",
subtitle: "因果關係必須嚴謹",
icon: "🔄",
color: "bg-blue-50 border-blue-200",
content: "順向:若原因 X 發生,是否必然導致結果 Y逆向若消除原因 X結果 Y 是否就不會發生?",
principle: "若無法雙向通過,代表邏輯有斷層"
},
{
title: "止於可執行對策",
subtitle: "永久性對策,非暫時性",
icon: "✅",
color: "bg-violet-50 border-violet-200",
content: "當追問到可以透過具體行動來根除的層次時,就是停止追問的時刻。對策必須是「永久性」的,而非「重新訓練、加強宣導」等暫時性措施。",
principle: "目的是解決問題,不是寫報告"
}
];
const getLanguageName = (code) => {
const lang = languages.find((l) => l.code === code);
return lang ? lang.name : code;
};
const analyzeWith5Why = async () => {
if (!finding.trim() || !jobContent.trim()) {
setError("請填寫所有欄位");
return;
}
setLoading(true);
setError("");
setResults([]);
const langName = getLanguageName(outputLanguage);
const prompt = `你是一位專精於「5 Why 根因分析法」的資深顧問。請嚴格遵循以下五大執行要項進行分析:
## 五大執行要項
### 1. 精準定義問題(描述現象,而非結論)
- 第一步必須客觀描述「發生了什麼事」,而非直接跳入「我認為是甚麼問題」
- 具體化包含人、事、時、地、物5W1H
### 2. 聚焦於「流程」與「系統」,而非「人」
- 若答案是「人為疏失」,請繼續追問:「為什麼系統允許這個疏失發生?」
- 原則:解決問題的機制,而非責備犯錯的人
### 3. 基於「事實」與「現場」,拒絕「猜測」
- 每一個「為什麼」的回答,都必須是可查證的事實
- 若無法確認,應標註需要驗證的假設
### 4. 邏輯的「雙向檢核」
- 順向檢查:若原因 X 發生,是否必然導致結果 Y
- 逆向檢查:若消除了原因 X結果 Y 是否就不會發生?
### 5. 止於「可執行的對策」
- 根本原因必須能對應到一個「永久性對策」(不再發生)
- 不僅是「暫時性對策」(如:重新訓練、加強宣導)
---
## 待分析內容
**Finding發現的問題/現象):** ${finding}
**工作內容背景:** ${jobContent}
---
## 輸出要求
請提供 **三個不同角度** 的 5 Why 分析,每個分析從不同的切入點出發(例如:流程面、系統面、管理面、設備面、環境面等)。
注意:
- 5 Why 的目的不是「湊滿五個問題」,而是穿透表面症狀直達根本原因
- 若在第 3 或第 4 個 Why 就已找到真正的根本原因,可以停止(設為 null
- 每個 Why 必須標註是「已驗證事實」還是「待驗證假設」
- 最終對策必須是「永久性對策」
⚠️ 重要:請使用 **${langName}** 語言回覆所有內容。
請用以下 JSON 格式回覆(不要加任何 markdown 標記):
{
"problemRestatement": "根據 5W1H 重新描述的問題定義",
"analyses": [
{
"perspective": "分析角度(如:流程面)",
"perspectiveIcon": "適合的 emoji",
"whys": [
{
"level": 1,
"question": "為什麼...?",
"answer": "因為...",
"isVerified": true,
"verificationNote": "已確認/需驗證:說明"
}
],
"rootCause": "根本原因(系統/流程層面)",
"logicCheck": {
"forward": "順向檢核:如果[原因]發生,則[結果]必然發生",
"backward": "逆向檢核:如果消除[原因],則[結果]不會發生",
"isValid": true
},
"countermeasure": {
"permanent": "永久性對策(系統性解決方案)",
"actionItems": ["具體行動項目1", "具體行動項目2"],
"avoidList": ["避免的暫時性做法(如:加強宣導)"]
}
}
]
}`;
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 6000,
messages: [{ role: "user", content: prompt }],
}),
});
const data = await response.json();
const text = data.content
.map((item) => (item.type === "text" ? item.text : ""))
.join("");
const clean = text.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(clean);
setResults(parsed);
setCurrentLanguage(outputLanguage);
} catch (err) {
setError("分析失敗,請稍後再試:" + err.message);
} finally {
setLoading(false);
}
};
const translateResults = async (targetLang) => {
if (!results.analyses || targetLang === currentLanguage) return;
setTranslating(true);
setError("");
const langName = getLanguageName(targetLang);
const prompt = `請將以下 5 Why 分析結果翻譯成 **${langName}**。
原始內容:
${JSON.stringify(results, null, 2)}
請保持完全相同的 JSON 結構,只翻譯文字內容。
請用以下 JSON 格式回覆(不要加任何 markdown 標記):
{
"problemRestatement": "...",
"analyses": [...]
}`;
try {
const response = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 6000,
messages: [{ role: "user", content: prompt }],
}),
});
const data = await response.json();
const text = data.content
.map((item) => (item.type === "text" ? item.text : ""))
.join("");
const clean = text.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(clean);
setResults(parsed);
setCurrentLanguage(targetLang);
} catch (err) {
setError("翻譯失敗:" + err.message);
} finally {
setTranslating(false);
}
};
const cardColors = [
{ header: "bg-blue-500", headerText: "text-white", border: "border-blue-200" },
{ header: "bg-violet-500", headerText: "text-white", border: "border-violet-200" },
{ header: "bg-teal-500", headerText: "text-white", border: "border-teal-200" },
];
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 p-4 md:p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl md:text-4xl font-bold text-slate-800 mb-2">
🔍 5 Why 根因分析器
</h1>
<p className="text-slate-500 text-sm md:text-base">
穿透問題表面直達根本原因產出永久性對策
</p>
</div>
{/* Guidelines Toggle */}
<div className="mb-6">
<button
onClick={() => setShowGuide(!showGuide)}
className="w-full py-3 bg-white hover:bg-slate-50 border border-slate-200 rounded-xl text-slate-600 font-medium transition-all flex items-center justify-center gap-2 shadow-sm"
>
📚 5 Why 執行要項指南
<span className={`transition-transform ${showGuide ? "rotate-180" : ""}`}></span>
</button>
{showGuide && (
<div className="mt-4 grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{guidelines.map((guide, idx) => (
<div key={idx} className={`${guide.color} rounded-xl p-4 border shadow-sm`}>
<div className="flex items-start gap-3 mb-3">
<span className="text-2xl">{guide.icon}</span>
<div>
<h3 className="text-slate-800 font-bold">{guide.title}</h3>
<p className="text-slate-500 text-sm">{guide.subtitle}</p>
</div>
</div>
<p className="text-slate-600 text-sm mb-3">{guide.content}</p>
{guide.example && (
<div className="bg-white/70 rounded-lg p-3 text-xs">
<div className="text-red-600 mb-1"> {guide.example.bad}</div>
<div className="text-emerald-600"> {guide.example.good}</div>
</div>
)}
{guide.principle && (
<div className="bg-white/70 rounded-lg p-2 text-xs text-blue-700 border border-blue-200">
💡 {guide.principle}
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Input Section */}
<div className="bg-white rounded-2xl p-6 mb-6 border border-slate-200 shadow-sm">
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
📋 我負責的 Finding 是什麼
</label>
<p className="text-xs text-slate-400 mb-2">
請具體描述現象5W1H何人何事何時何地如何發生
</p>
<textarea
value={finding}
onChange={(e) => setFinding(e.target.value)}
placeholder="範例A 機台在 12/5 下午 2 點運轉時,主軸溫度達 95°C 觸發過熱保護導致停機,影響當日產能 200 件..."
className="w-full h-36 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
💼 我的工作內容是什麼
</label>
<p className="text-xs text-slate-400 mb-2">
說明您的職責範圍幫助分析聚焦在可控因素
</p>
<textarea
value={jobContent}
onChange={(e) => setJobContent(e.target.value)}
placeholder="範例:負責生產線設備維護與品質管控,管理 5 台 CNC 機台,需確保 OEE 達 85% 以上..."
className="w-full h-36 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm"
/>
</div>
</div>
{/* Language Selection */}
<div className="mb-6">
<label className="block text-sm font-medium text-slate-700 mb-2">
🌐 輸出語言
</label>
<div className="flex flex-wrap gap-2">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => setOutputLanguage(lang.code)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
outputLanguage === lang.code
? "bg-blue-500 text-white shadow-md"
: "bg-slate-100 text-slate-600 hover:bg-slate-200"
}`}
>
{lang.flag} {lang.name}
</button>
))}
</div>
</div>
<button
onClick={analyzeWith5Why}
disabled={loading}
className="w-full py-4 bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 disabled:from-slate-300 disabled:to-slate-300 text-white font-bold rounded-xl transition-all duration-300 transform hover:scale-[1.02] disabled:scale-100 disabled:cursor-not-allowed shadow-lg"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
深度分析中...
</span>
) : (
"🎯 Find My 5 Why"
)}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm">
{error}
</div>
)}
</div>
{/* Problem Restatement */}
{results.problemRestatement && (
<div className="bg-indigo-50 rounded-2xl p-5 mb-6 border border-indigo-200 shadow-sm">
<h3 className="text-indigo-700 font-bold mb-2 flex items-center gap-2">
📝 問題重新定義5W1H
</h3>
<p className="text-indigo-900">{results.problemRestatement}</p>
</div>
)}
{/* Translation Bar */}
{results.analyses && results.analyses.length > 0 && (
<div className="bg-white rounded-xl p-4 mb-6 border border-slate-200 shadow-sm">
<div className="flex flex-wrap items-center gap-3">
<span className="text-slate-600 text-sm font-medium">🔄 翻譯</span>
<span className="text-slate-500 text-sm">
目前
<span className="text-blue-600 font-medium ml-1">
{languages.find((l) => l.code === currentLanguage)?.flag} {getLanguageName(currentLanguage)}
</span>
</span>
<div className="flex-1" />
<div className="flex flex-wrap gap-2">
{languages.filter((l) => l.code !== currentLanguage).map((lang) => (
<button
key={lang.code}
onClick={() => translateResults(lang.code)}
disabled={translating}
className="px-3 py-1.5 bg-slate-100 hover:bg-slate-200 disabled:bg-slate-50 text-slate-600 text-sm rounded-lg transition-all disabled:cursor-not-allowed"
>
{lang.flag} {lang.name}
</button>
))}
</div>
{translating && (
<span className="flex items-center gap-2 text-blue-500 text-sm">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
翻譯中...
</span>
)}
</div>
</div>
)}
{/* Results Section */}
{results.analyses && results.analyses.length > 0 && (
<div className="grid lg:grid-cols-3 gap-6">
{results.analyses.map((analysis, idx) => (
<div key={idx} className={`bg-white rounded-2xl border ${cardColors[idx].border} overflow-hidden shadow-sm`}>
{/* Card Header */}
<div className={`p-4 ${cardColors[idx].header}`}>
<h3 className={`text-lg font-bold ${cardColors[idx].headerText} flex items-center gap-2`}>
<span className="text-2xl">{analysis.perspectiveIcon || "📊"}</span>
{analysis.perspective}
</h3>
</div>
{/* 5 Whys */}
<div className="p-4 space-y-3">
{analysis.whys.filter(w => w !== null).map((why, wIdx) => (
<div
key={wIdx}
className="bg-slate-50 rounded-lg p-3 border-l-4"
style={{ borderLeftColor: `hsl(${200 + wIdx * 30}, 70%, 50%)` }}
>
<div className="flex items-start gap-2">
<div className="flex flex-col items-center">
<span className="bg-slate-200 text-xs px-2 py-1 rounded font-mono text-slate-600">
W{why.level}
</span>
<span className={`mt-1 text-xs ${why.isVerified ? "text-emerald-500" : "text-amber-500"}`}>
{why.isVerified ? "✓" : "?"}
</span>
</div>
<div className="flex-1">
<p className="text-slate-700 text-sm font-medium mb-1">{why.question}</p>
<p className="text-slate-500 text-sm">{why.answer}</p>
{why.verificationNote && (
<p className={`text-xs mt-1 ${why.isVerified ? "text-emerald-500" : "text-amber-500"}`}>
{why.verificationNote}
</p>
)}
</div>
</div>
</div>
))}
{/* Root Cause */}
<div className="mt-4 p-4 bg-amber-50 rounded-xl border border-amber-200">
<h4 className="text-amber-700 font-bold text-sm mb-2">🎯 根本原因</h4>
<p className="text-amber-900 text-sm">{analysis.rootCause}</p>
</div>
{/* Logic Check */}
{analysis.logicCheck && (
<div className="p-4 bg-slate-50 rounded-xl border border-slate-200">
<h4 className="text-slate-700 font-bold text-sm mb-2 flex items-center gap-2">
🔄 邏輯雙向檢核
<span className={`text-xs px-2 py-0.5 rounded text-white ${analysis.logicCheck.isValid ? "bg-emerald-500" : "bg-red-500"}`}>
{analysis.logicCheck.isValid ? "通過" : "需檢視"}
</span>
</h4>
<div className="space-y-2 text-xs">
<div className="text-slate-600">
<span className="text-blue-600 font-medium">順向</span> {analysis.logicCheck.forward}
</div>
<div className="text-slate-600">
<span className="text-violet-600 font-medium">逆向</span> {analysis.logicCheck.backward}
</div>
</div>
</div>
)}
{/* Countermeasure */}
{analysis.countermeasure && (
<div className="p-4 bg-emerald-50 rounded-xl border border-emerald-200">
<h4 className="text-emerald-700 font-bold text-sm mb-2"> 永久性對策</h4>
<p className="text-emerald-900 text-sm mb-3">{analysis.countermeasure.permanent}</p>
{analysis.countermeasure.actionItems && (
<div className="mb-3">
<p className="text-emerald-700 text-xs font-medium mb-1">行動項目</p>
<ul className="space-y-1">
{analysis.countermeasure.actionItems.map((item, i) => (
<li key={i} className="text-emerald-800 text-xs flex items-start gap-1">
<span></span> {item}
</li>
))}
</ul>
</div>
)}
{analysis.countermeasure.avoidList && analysis.countermeasure.avoidList.length > 0 && (
<div className="bg-red-50 rounded-lg p-2 border border-red-200">
<p className="text-red-600 text-xs font-medium mb-1"> 避免暫時性做法</p>
<ul className="space-y-1">
{analysis.countermeasure.avoidList.map((item, i) => (
<li key={i} className="text-red-500 text-xs flex items-start gap-1">
<span></span> {item}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Empty State */}
{(!results.analyses || results.analyses.length === 0) && !loading && (
<div className="text-center py-16 text-slate-400">
<div className="text-6xl mb-4">🤔</div>
<p className="mb-2 text-slate-500">輸入問題現象與工作內容</p>
<p className="text-sm">系統將依據 5 Why 方法論進行深度根因分析</p>
</div>
)}
{/* Footer */}
<div className="mt-8 text-center text-slate-400 text-xs">
💡 5 Why 的目的不是湊滿五個問題而是穿透表面症狀直達根本原因
</div>
</div>
</div>
);
}

122
README.md Normal file
View File

@@ -0,0 +1,122 @@
# 5 Why 根因分析器
這是一個基於 5 Why 方法論的根因分析工具,使用 Ollama API (qwen2.5:3b 模型) 進行智能分析。
## 功能特點
- ✅ 使用 5 Why 方法進行深度根因分析
- ✅ 從三個不同角度進行分析(流程面、系統面、管理面等)
- ✅ 支援多語言輸出(繁中、簡中、英文、日文、韓文、越南文、泰文)
- ✅ 提供執行要項指南
- ✅ 邏輯雙向檢核
- ✅ 產出永久性對策建議
## 技術架構
### 後端
- Node.js + Express
- Ollama API 整合
- API URL: `https://ollama_pjapi.theaken.com`
- 模型: `qwen2.5:3b`
### 前端
- React 18
- Vite
- Tailwind CSS
- 響應式設計
## 安裝與執行
### 1. 安裝依賴
```bash
npm install
```
### 2. 啟動應用(同時啟動前端和後端)
```bash
npm run dev
```
這會同時啟動:
- 後端服務器: http://localhost:3001
- 前端開發服務器: http://localhost:5173
### 3. 單獨啟動
如果需要單獨啟動:
```bash
# 只啟動後端
npm run server
# 只啟動前端
npm run client
```
### 4. 生產環境建置
```bash
npm run build
```
## 使用說明
1. 填寫「Finding」具體描述問題現象使用 5W1H
2. 填寫「工作內容」:說明您的職責範圍
3. 選擇輸出語言
4. 點擊「Find My 5 Why」進行分析
5. 查看三個不同角度的分析結果
6. 可以使用翻譯功能切換不同語言
## API 端點
### 後端 API
- `GET /health` - 健康檢查
- `GET /api/models` - 列出可用的 Ollama 模型
- `POST /api/analyze` - 執行 5 Why 分析
- `POST /api/translate` - 翻譯分析結果
### 請求範例
```javascript
// 分析請求
fetch('http://localhost:3001/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: '您的分析提示...' })
})
```
## 專案結構
```
5why/
├── server.js # Express 後端服務器
├── package.json # 專案配置
├── vite.config.js # Vite 配置
├── tailwind.config.js # Tailwind CSS 配置
├── postcss.config.js # PostCSS 配置
├── index.html # HTML 入口
└── src/
├── main.jsx # React 入口
├── App.jsx # 主應用組件
├── FiveWhyAnalyzer.jsx # 5 Why 分析器組件
└── index.css # 全局樣式
```
## 注意事項
1. 確保 Ollama API 服務可用
2. 後端服務必須先啟動才能進行分析
3. 建議使用 Chrome 或 Firefox 瀏覽器以獲得最佳體驗
4. 分析時間可能需要 30-60 秒,請耐心等待
## 環境要求
- Node.js 16+
- npm 或 yarn
- 現代瀏覽器(支援 ES6+
## 授權
MIT License

343
README_FULL.md Normal file
View File

@@ -0,0 +1,343 @@
# 5 Why 根因分析器 (5 Why Root Cause Analyzer)
[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://github.com/yourusername/5why-analyzer)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
這是一個基於 5 Why 方法論的企業級根因分析工具,使用 Ollama API (qwen2.5:3b 模型) 進行智能分析。系統遵循完整的開發流程 SOP包含使用者管理、權限控制、資料庫整合等企業級功能。
## 📋 目錄
- [功能特點](#功能特點)
- [系統架構](#系統架構)
- [快速開始](#快速開始)
- [環境設定](#環境設定)
- [使用說明](#使用說明)
- [API 文件](#api-文件)
- [專案結構](#專案結構)
- [開發流程](#開發流程)
- [資安說明](#資安說明)
- [常見問題](#常見問題)
## ✨ 功能特點
### 核心分析功能
-**5 Why 深度分析**: 從三個不同角度進行根本原因分析
-**多語言支援**: 繁中、簡中、英文、日文、韓文、越南文、泰文
-**邏輯雙向檢核**: 驗證因果關係的嚴謹性
-**永久性對策**: 產出系統性解決方案,避免暫時性措施
-**執行要項指南**: 內建 5 Why 方法論最佳實踐指南
### 管理者功能(開發中)
- 👥 **使用者管理**: 工號、姓名、Email、權限等級
- 🔐 **三級權限控制**: 一般使用者 / 管理者 / 最高權限管理者
- 🤖 **LLM API 設定**: 支援 Gemini / DeepSeek / OpenAI / Ollama
- 📊 **系統參數設定**: 彈性的系統配置介面
### 通用功能(開發中)
- 📥 **CSV 匯入/匯出**: 所有資料表支援批次處理
- 🔍 **欄位排序**: 清單頁面支援多欄位排序
- ⚠️ **錯誤處理**: 統一的錯誤處理與通知機制
- 🔄 **Loading 指示器**: 改善使用者體驗
### 資安功能(規劃中)
- 🛡️ **SQL Injection 防護**: 參數化查詢
- 🔒 **XSS 防護**: 輸入驗證與輸出編碼
- 🔑 **密碼加密**: bcrypt 加密儲存
- 🚦 **API Rate Limiting**: 防止濫用
- 🎫 **Session 安全**: Secure cookie 設定
## 🏗️ 系統架構
### 後端技術棧
- **框架**: Node.js + Express
- **資料庫**: MySQL 8.0+ (規劃中)
- **AI 引擎**: Ollama API (qwen2.5:3b)
- **安全**: Helmet, bcryptjs, express-session
- **API**: RESTful API
### 前端技術棧
- **框架**: React 18
- **建置工具**: Vite
- **樣式**: Tailwind CSS
- **狀態管理**: React Hooks
- **響應式設計**: Mobile-first approach
### 資料庫架構(規劃中)
- `users` - 使用者資料表
- `analyses` - 分析記錄表
- `llm_configs` - LLM API 配置表
- `system_settings` - 系統設定表
- `audit_logs` - 稽核日誌表
詳細資料庫結構請參考 [docs/db_schema.md](docs/db_schema.md)(待建立)
## 🚀 快速開始
### 1. 環境需求
- Node.js 16.x 或更高版本
- MySQL 8.0 或更高版本(若需要資料庫功能)
- npm 或 yarn
- 現代瀏覽器Chrome、Firefox、Edge
### 2. 安裝步驟
```bash
# 1. 進入專案目錄
cd 5why
# 2. 安裝依賴
npm install
# 3. 設定環境變數(選擇性)
cp .env.example .env
# 編輯 .env 填入您的設定(當前版本可以不設定)
# 4. 啟動應用
npm run dev
```
### 3. 訪問應用
- 前端: http://localhost:5173
- 後端 API: http://localhost:3001
## ⚙️ 環境設定
### .env 配置說明(選擇性)
```bash
# Ollama API 設定
OLLAMA_API_URL=https://ollama_pjapi.theaken.com
OLLAMA_MODEL=qwen2.5:3b
# 伺服器設定
SERVER_PORT=3001
CLIENT_PORT=5173
```
完整配置說明請參考 [.env.example](.env.example)
## 📖 使用說明
### 基本使用流程
1. **填寫 Finding**: 具體描述問題現象(使用 5W1H
-Who涉及哪些人或部門
- 什麼What發生了什麼問題
- 何時When何時發生
- 何地Where在哪裡發生
- 為何Why初步判斷
- 如何How問題如何呈現
2. **說明工作內容**: 填寫您的職責範圍,幫助 AI 聚焦在可控因素
3. **選擇輸出語言**: 支援 7 種語言
- 🇹🇼 繁體中文
- 🇨🇳 简体中文
- 🇺🇸 English
- 🇯🇵 日本語
- 🇰🇷 한국어
- 🇻🇳 Tiếng Việt
- 🇹🇭 ภาษาไทย
4. **執行分析**: 點擊「Find My 5 Why」按鈕
5. **查看結果**: 系統會從三個不同角度進行分析:
- 流程面分析
- 系統面分析
- 管理面分析(或其他相關角度)
6. **切換語言**: 使用翻譯功能即時切換分析結果的語言
### 5 Why 執行要項指南
系統內建完整的 5 Why 方法論指南,包含:
1. **精準定義問題**: 描述現象,而非結論
2. **聚焦流程與系統**: 而非責備個人
3. **基於事實與現場**: 拒絕猜測
4. **邏輯雙向檢核**: 因果關係必須嚴謹
5. **止於可執行對策**: 永久性對策,非暫時性
## 🔌 API 文件
### 當前可用端點
```
GET /health - 健康檢查
GET /api/models - 列出可用的 Ollama 模型
POST /api/analyze - 執行 5 Why 分析
POST /api/translate - 翻譯分析結果
```
### API 請求範例
#### 執行分析
```javascript
const response = await fetch('http://localhost:3001/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '您的完整分析提示...'
})
});
const data = await response.json();
console.log(data.content); // AI 回應內容
```
#### 翻譯結果
```javascript
const response = await fetch('http://localhost:3001/api/translate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: '翻譯提示...'
})
});
const data = await response.json();
console.log(data.content); // 翻譯後的內容
```
完整 API 文件請參考 [docs/API_DOC.md](docs/API_DOC.md)(待建立)
## 📁 專案結構
```
5why-analyzer/
├── server.js # Express 主程式
├── package.json # 專案配置
├── .env.example # 環境變數範本
├── .gitignore # Git 忽略清單
├── models/ # 資料庫模型(待建立)
├── routes/ # API 路由(待建立)
├── middleware/ # 中介層(待建立)
├── templates/ # 前端模板(待建立)
├── static/ # 靜態資源
│ ├── css/
│ ├── js/
│ └── images/
├── src/ # React 前端
│ ├── main.jsx # React 入口
│ ├── App.jsx # 主應用組件
│ ├── FiveWhyAnalyzer.jsx # 5 Why 分析器組件
│ └── index.css # 全局樣式
├── docs/ # 文件目錄
│ ├── user_command_log.md # 指令紀錄
│ ├── SDD.md # 系統設計文件(待建立)
│ ├── db_schema.md # 資料庫架構(待建立)
│ ├── API_DOC.md # API 文件(待建立)
│ ├── CHANGELOG.md # 變更紀錄(待建立)
│ └── security_audit.md # 資安稽核(待建立)
├── index.html # HTML 入口
├── vite.config.js # Vite 配置
├── tailwind.config.js # Tailwind CSS 配置
├── postcss.config.js # PostCSS 配置
└── README.md # 本文件
```
## 🔄 開發流程
本專案遵循完整的開發流程 SOP
-**Phase 0**: 專案初始化 (完成)
- ✅ 建立專案資料夾結構
- ✅ 建立 .env.example
- ✅ 建立 .gitignore
- ✅ 更新 package.json
- ✅ 建立 README.md
- 🔄 **Phase 1**: 版本控制設定(待用戶提供 Gitea 資訊)
- 🔄 **Phase 2**: 資料庫架構設計(待用戶提供 MySQL 資訊)
-**Phase 3**: UI/UX 預覽確認
-**Phase 4**: 核心程式開發
-**Phase 5**: 管理者功能開發
-**Phase 6**: 通用功能實作
-**Phase 7**: 資安檢視
-**Phase 8**: 文件維護
-**Phase 9**: 部署前檢查
進度追蹤請參考 [docs/user_command_log.md](docs/user_command_log.md)
## 🛡️ 資安說明
本系統將實施多層次資安防護Phase 7 實作):
1. **輸入驗證**: 所有用戶輸入進行驗證與淨化
2. **SQL Injection 防護**: 使用參數化查詢
3. **XSS 防護**: 輸出編碼與 Content Security Policy
4. **CSRF 防護**: CSRF Token 機制
5. **密碼安全**: bcrypt 加密 + 密碼強度檢查
6. **Session 安全**: HttpOnly + Secure + SameSite cookies
7. **Rate Limiting**: API 請求頻率限制
8. **Audit Logging**: 所有操作記錄
資安稽核報告將在 Phase 7 產出:[docs/security_audit.md](docs/security_audit.md)
## ❓ 常見問題
### Q: 分析時間過長怎麼辦?
A: Ollama API 回應時間約 30-60 秒,這是正常現象。請耐心等待,系統會顯示 Loading 動畫。
### Q: 為什麼分析結果是英文?
A: 請確認您在「輸出語言」選項中選擇了正確的語言。預設是繁體中文。
### Q: 可以離線使用嗎?
A: 目前不支援,因為需要連線至 Ollama API。未來可能會支援本地部署。
### Q: 分析結果準確嗎?
A: 分析結果由 AI 生成,僅供參考。建議結合實際情況進行驗證和調整。
### Q: 支援哪些瀏覽器?
A: 建議使用 Chrome、Firefox 或 Edge 的最新版本。不建議使用 IE。
### Q: 如何更換 AI 模型?
A: 目前固定使用 qwen2.5:3b。管理者功能完成後可在管理後台切換其他模型。
## 🚧 待開發功能
以下功能將在後續 Phase 開發:
- [ ] 使用者登入/註冊系統
- [ ] 使用者權限管理
- [ ] 分析歷史記錄
- [ ] CSV 匯入/匯出
- [ ] 多 LLM 支援Gemini、DeepSeek、OpenAI
- [ ] 資料庫整合
- [ ] 稽核日誌
- [ ] PDF 報告匯出
- [ ] 批次分析功能
## 📝 授權
MIT License
## 🤝 貢獻
歡迎提交 Issue 和 Pull Request
開發指南:
1. Fork 本專案
2. 建立您的功能分支 (`git checkout -b feature/AmazingFeature`)
3. 提交您的修改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 開啟 Pull Request
## 📞 聯絡方式
如有問題或建議,請:
- 提交 Issue
- 聯繫專案維護者
---
**版本**: v1.0.0
**最後更新**: 2025-12-05
**開發狀態**: Phase 0 完成Phase 1-9 進行中
**授權**: MIT License

102
config.js Normal file
View File

@@ -0,0 +1,102 @@
import dotenv from 'dotenv';
import mysql from 'mysql2/promise';
// 載入環境變數
dotenv.config();
// 資料庫配置
export const dbConfig = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 3306,
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'db_A102',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0
};
// 建立連線池
export const pool = mysql.createPool(dbConfig);
// 測試資料庫連線
export async function testConnection() {
try {
const connection = await pool.getConnection();
console.log('✅ Database connected successfully');
console.log(` Host: ${dbConfig.host}:${dbConfig.port}`);
console.log(` Database: ${dbConfig.database}`);
connection.release();
return true;
} catch (error) {
console.error('❌ Database connection failed:', error.message);
return false;
}
}
// 執行查詢的輔助函數
export async function query(sql, params = []) {
try {
const [results] = await pool.execute(sql, params);
return results;
} catch (error) {
console.error('Query error:', error.message);
throw error;
}
}
// Ollama API 配置
export const ollamaConfig = {
apiUrl: process.env.OLLAMA_API_URL || 'https://ollama_pjapi.theaken.com',
model: process.env.OLLAMA_MODEL || 'qwen2.5:3b',
maxTokens: 6000,
temperature: 0.7,
timeout: 120000
};
// 伺服器配置
export const serverConfig = {
host: process.env.SERVER_HOST || 'localhost',
port: parseInt(process.env.SERVER_PORT) || 3001,
clientPort: parseInt(process.env.CLIENT_PORT) || 5173
};
// Session 配置
export const sessionConfig = {
secret: process.env.SESSION_SECRET || 'change-this-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
};
// 安全配置
export const securityConfig = {
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS) || 10,
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX) || 100,
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 900000 // 15 minutes
};
// Gitea 配置
export const giteaConfig = {
url: process.env.GITEA_URL || 'https://gitea.theaken.com/',
user: process.env.GITEA_USER || '',
token: process.env.GITEA_TOKEN || ''
};
export default {
dbConfig,
pool,
testConnection,
query,
ollamaConfig,
serverConfig,
sessionConfig,
securityConfig,
giteaConfig
};

229
docs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,229 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [Unreleased]
### Planned Features
- [ ] User authentication and authorization system
- [ ] Admin dashboard with user management
- [ ] Analysis history with pagination
- [ ] CSV import/export functionality
- [ ] Multi-LLM support (Gemini, DeepSeek, OpenAI)
- [ ] PDF report generation
- [ ] Batch analysis functionality
- [ ] Email notifications
- [ ] Advanced search and filtering
- [ ] API rate limiting per user
- [ ] Two-factor authentication
---
## [1.0.0] - 2025-12-05
### Added (Phase 0: 專案初始化)
- ✅ Project folder structure created
- `models/` - Database models directory
- `routes/` - API routes directory
- `templates/` - Frontend templates directory
- `static/` - Static assets (css, js, images)
- `docs/` - Documentation directory
- `scripts/` - Utility scripts directory
- ✅ Environment configuration
- Created `.env.example` with all required environment variables
- Created `.env` with actual configuration
- Added `dotenv` package for environment management
- ✅ Version control setup
- Created `.gitignore` for Node.js, Python, and IDE files
- Excluded sensitive files (.env, security_audit.md)
- Ready for Git initialization
- ✅ Dependencies management
- Updated `package.json` with enterprise-grade packages:
- Security: `bcryptjs`, `helmet`, `express-rate-limit`
- Database: `mysql2` with connection pooling
- Session: `express-session`
- CSV: `csv-parser`, `json2csv`
- Added scripts: `db:init`, `db:test`
- ✅ Documentation
- Created comprehensive `README_FULL.md`
- Created `docs/user_command_log.md` for tracking user requests
- Documented all completed Phase 0 tasks
### Added (Phase 2: 資料庫架構)
- ✅ Database configuration
- Created `config.js` with database connection pool
- MySQL connection details configured
- Connection testing functionality
- ✅ Database schema design
- Created `docs/db_schema.sql` with complete table definitions:
- `users` - User management with 3-tier permissions
- `analyses` - Analysis records with JSON storage
- `analysis_perspectives` - Multiple perspective analysis
- `analysis_whys` - Detailed 5 Why records
- `llm_configs` - LLM API configurations
- `system_settings` - System parameters
- `audit_logs` - Security audit trail
- `sessions` - User session management
- Created views:
- `user_analysis_stats` - User statistics dashboard
- `recent_analyses` - Recent 100 analyses
- ✅ Database documentation
- Created comprehensive `docs/db_schema.md`
- Detailed table descriptions with field explanations
- Entity relationship diagrams
- Index strategy documentation
- Data dictionary with code mappings
- ✅ Database initialization
- Created `scripts/init-database.js` for schema setup
- Created `scripts/init-database-simple.js` (simplified version)
- Created `scripts/test-db-connection.js` for testing
- Successfully initialized 8 core tables + 2 views
- Inserted default data:
- 3 demo users (admin, user001, user002)
- 1 Ollama LLM configuration
- 6 system settings
### Technical Details
- **Database**: MySQL 9.4.0 at mysql.theaken.com:33306
- **Database Name**: db_A102
- **Character Set**: utf8mb4_unicode_ci
- **Engine**: InnoDB with foreign key constraints
- **Default Admin**: admin@example.com (password in .env)
### Files Added
```
5why/
├── .env # Environment variables
├── .env.example # Environment template
├── .gitignore # Git ignore rules
├── config.js # Configuration module
├── package.json # Updated with new dependencies
├── docs/
│ ├── db_schema.sql # Database schema SQL
│ ├── db_schema.md # Database documentation
│ ├── user_command_log.md # User command tracking
│ └── CHANGELOG.md # This file
├── scripts/
│ ├── init-database.js # DB initialization script
│ ├── init-database-simple.js # Simplified DB init
│ └── test-db-connection.js # DB connection test
└── README_FULL.md # Comprehensive README
```
### Database Tables Created
1. `users` - 3 rows (1 admin, 2 test users)
2. `analyses` - 0 rows
3. `analysis_perspectives` - 0 rows
4. `analysis_whys` - 0 rows
5. `llm_configs` - 1 row (Ollama config)
6. `system_settings` - 6 rows
7. `audit_logs` - 0 rows
8. `sessions` - 0 rows
9. `user_analysis_stats` (view)
10. `recent_analyses` (view)
### Dependencies Added
- `dotenv@^16.3.1` - Environment variables
- `bcryptjs@^2.4.3` - Password encryption
- `express-session@^1.17.3` - Session management
- `express-rate-limit@^7.1.5` - API rate limiting
- `mysql2@^3.6.5` - MySQL database driver
- `helmet@^7.1.0` - Security headers
- `csv-parser@^3.0.0` - CSV import
- `json2csv@^6.0.0-alpha.2` - CSV export
### Configuration
- Gitea Repository: https://gitea.theaken.com/
- Gitea User: donald
- Database Host: mysql.theaken.com:33306
- Ollama API: https://ollama_pjapi.theaken.com
- Model: qwen2.5:3b
---
## Next Steps (Phase 1-9)
### Phase 1: 版本控制設定 (Pending)
- [ ] Initialize Git repository
- [ ] Create Gitea remote repository
- [ ] Configure Git remote origin
- [ ] Create `.gitkeep` in empty folders
- [ ] Initial commit and push
### Phase 3: UI/UX 預覽確認 (Pending)
- [ ] Create `preview.html` (frontend only, no database)
- [ ] Confirm UI/UX design with user
- [ ] Get user approval before proceeding
### Phase 4: 核心程式開發 (Pending)
- [ ] Create `app.js` or enhanced `server.js`
- [ ] Implement database models in `models/`
- [ ] Implement API routes in `routes/`
- [ ] Integrate with database
- [ ] Add error handling
- [ ] Add logging
### Phase 5: 管理者功能開發 (Pending)
- [ ] Admin dashboard at `/admin`
- [ ] User management (CRUD)
- [ ] LLM configuration interface
- [ ] System settings interface
- [ ] Audit log viewer
### Phase 6: 通用功能實作 (Pending)
- [ ] Error handling modal
- [ ] CSV import/export for all tables
- [ ] Column sorting on list pages
- [ ] Loading indicators
- [ ] Success/failure notifications
### Phase 7: 資安檢視 (Pending)
- [ ] Create `security_audit.md`
- [ ] Check SQL Injection protection
- [ ] Check XSS protection
- [ ] Verify CSRF tokens
- [ ] Verify password encryption
- [ ] Verify API rate limiting
- [ ] Check for sensitive information leaks
- [ ] Verify session security
### Phase 8: 文件維護 (Pending)
- [ ] Create/update `SDD.md` with version number
- [ ] Update `user_command_log.md`
- [ ] Update `CHANGELOG.md` (this file)
- [ ] Create `API_DOC.md`
### Phase 9: 部署前檢查 (Pending)
- [ ] Verify `.env.example` is complete
- [ ] Update `requirements.txt` or `package.json`
- [ ] Remove sensitive information from code
- [ ] Run functionality tests
- [ ] Final commit and push to Gitea
---
## Version History
| Version | Date | Status | Description |
|---------|------|--------|-------------|
| 1.0.0 | 2025-12-05 | In Progress | Initial version with Phase 0 & 2 completed |
| 0.1.0 | 2025-12-05 | Prototype | Basic React frontend with Ollama API |
---
**Maintainer**: System Administrator
**Last Updated**: 2025-12-05
**Document Version**: 1.0.0

416
docs/db_schema.md Normal file
View File

@@ -0,0 +1,416 @@
# 資料庫架構文件 (Database Schema)
**專案**: 5 Why Root Cause Analyzer
**資料庫**: db_A102
**版本**: 1.0.0
**建立日期**: 2025-12-05
**最後更新**: 2025-12-05
---
## 目錄
- [概述](#概述)
- [資料表清單](#資料表清單)
- [資料表詳細說明](#資料表詳細說明)
- [關聯圖](#關聯圖)
- [索引策略](#索引策略)
- [視圖說明](#視圖說明)
- [資料字典](#資料字典)
---
## 概述
本系統使用 MySQL 8.0+ 資料庫,採用 InnoDB 引擎支援交易處理和外鍵約束。資料庫設計遵循第三正規化3NF確保資料一致性和完整性。
### 連線資訊
- **主機**: mysql.theaken.com
- **Port**: 33306
- **資料庫**: db_A102
- **字元集**: utf8mb4
- **排序規則**: utf8mb4_unicode_ci
---
## 資料表清單
| # | 資料表名稱 | 說明 | 記錄數(預估) |
|---|-----------|------|--------------|
| 1 | `users` | 使用者資料表 | 100-1000 |
| 2 | `analyses` | 分析記錄表 | 10000+ |
| 3 | `analysis_perspectives` | 分析角度詳細表 | 30000+ |
| 4 | `analysis_whys` | 5 Why詳細記錄表 | 150000+ |
| 5 | `llm_configs` | LLM API配置表 | 10-50 |
| 6 | `system_settings` | 系統設定表 | 50-100 |
| 7 | `audit_logs` | 稽核日誌表 | 100000+ |
| 8 | `sessions` | Session表 | 100-1000 |
---
## 資料表詳細說明
### 1. users (使用者資料表)
儲存系統使用者的基本資料和認證資訊。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 使用者ID主鍵 |
| `employee_id` | VARCHAR(50) | UNIQUE, NOT NULL | 工號 |
| `username` | VARCHAR(100) | NOT NULL | 使用者名稱 |
| `email` | VARCHAR(255) | UNIQUE, NOT NULL | Email |
| `password_hash` | VARCHAR(255) | NOT NULL | 密碼雜湊bcrypt |
| `role` | ENUM | DEFAULT 'user' | 權限等級user/admin/super_admin |
| `department` | VARCHAR(100) | NULL | 部門 |
| `position` | VARCHAR(100) | NULL | 職位 |
| `is_active` | BOOLEAN | DEFAULT TRUE | 帳號啟用狀態 |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
| `updated_at` | TIMESTAMP | AUTO UPDATE | 更新時間 |
| `last_login_at` | TIMESTAMP | NULL | 最後登入時間 |
**索引**:
- PRIMARY KEY: `id`
- UNIQUE KEY: `employee_id`, `email`
- INDEX: `idx_employee_id`, `idx_email`, `idx_role`
**權限等級說明**:
- `user`: 一般使用者(可執行分析、查看自己的記錄)
- `admin`: 管理者(可管理使用者、查看所有記錄)
- `super_admin`: 最高權限管理者(完整系統控制權)
---
### 2. analyses (分析記錄表)
儲存每次 5 Why 分析的主要資料。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 分析記錄ID |
| `user_id` | INT | FK, NOT NULL | 使用者ID外鍵 |
| `finding` | TEXT | NOT NULL | Finding描述 |
| `job_content` | TEXT | NOT NULL | 工作內容 |
| `output_language` | VARCHAR(10) | DEFAULT 'zh-TW' | 輸出語言 |
| `problem_restatement` | TEXT | NULL | 問題重述5W1H |
| `analysis_result` | JSON | NULL | 完整分析結果JSON格式 |
| `status` | ENUM | DEFAULT 'pending' | 分析狀態 |
| `error_message` | TEXT | NULL | 錯誤訊息 |
| `processing_time` | INT | NULL | 處理時間(秒) |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
| `updated_at` | TIMESTAMP | AUTO UPDATE | 更新時間 |
**索引**:
- PRIMARY KEY: `id`
- FOREIGN KEY: `user_id` REFERENCES `users(id)`
- INDEX: `idx_user_id`, `idx_status`, `idx_created_at`
**狀態說明**:
- `pending`: 待處理
- `processing`: 處理中
- `completed`: 完成
- `failed`: 失敗
---
### 3. analysis_perspectives (分析角度詳細表)
儲存每個分析的不同角度(流程面、系統面、管理面等)的詳細資料。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 分析角度ID |
| `analysis_id` | INT | FK, NOT NULL | 分析記錄ID外鍵 |
| `perspective` | VARCHAR(100) | NOT NULL | 分析角度名稱 |
| `perspective_icon` | VARCHAR(10) | NULL | Emoji圖示 |
| `root_cause` | TEXT | NULL | 根本原因 |
| `permanent_solution` | TEXT | NULL | 永久性對策 |
| `logic_check_forward` | TEXT | NULL | 順向邏輯檢核 |
| `logic_check_backward` | TEXT | NULL | 逆向邏輯檢核 |
| `logic_valid` | BOOLEAN | DEFAULT TRUE | 邏輯是否有效 |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
**索引**:
- PRIMARY KEY: `id`
- FOREIGN KEY: `analysis_id` REFERENCES `analyses(id)`
- INDEX: `idx_analysis_id`
---
### 4. analysis_whys (5 Why詳細記錄表)
儲存每個分析角度的 5 Why 問答詳細記錄。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | Why記錄ID |
| `perspective_id` | INT | FK, NOT NULL | 分析角度ID外鍵 |
| `level` | INT | NOT NULL | Why層級1-5 |
| `question` | TEXT | NOT NULL | 問題 |
| `answer` | TEXT | NOT NULL | 答案 |
| `is_verified` | BOOLEAN | DEFAULT FALSE | 是否已驗證 |
| `verification_note` | TEXT | NULL | 驗證說明 |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
**索引**:
- PRIMARY KEY: `id`
- FOREIGN KEY: `perspective_id` REFERENCES `analysis_perspectives(id)`
- INDEX: `idx_perspective_id`, `idx_level`
---
### 5. llm_configs (LLM API配置表)
儲存不同 LLM 提供商的 API 配置資訊。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 配置ID |
| `provider` | VARCHAR(50) | NOT NULL | LLM提供商 |
| `api_url` | VARCHAR(255) | NULL | API URL |
| `api_key` | VARCHAR(255) | NULL | API Key加密 |
| `model_name` | VARCHAR(100) | NULL | 模型名稱 |
| `is_active` | BOOLEAN | DEFAULT FALSE | 是否啟用 |
| `max_tokens` | INT | DEFAULT 6000 | 最大Token數 |
| `temperature` | DECIMAL(3,2) | DEFAULT 0.7 | 溫度參數 |
| `timeout` | INT | DEFAULT 120000 | Timeout毫秒 |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
| `updated_at` | TIMESTAMP | AUTO UPDATE | 更新時間 |
| `created_by` | INT | FK, NULL | 建立者ID |
**索引**:
- PRIMARY KEY: `id`
- FOREIGN KEY: `created_by` REFERENCES `users(id)`
- INDEX: `idx_provider`, `idx_is_active`
- UNIQUE KEY: `unique_active_provider` (provider, is_active)
**支援的 Provider**:
- `ollama`: Ollama API
- `gemini`: Google Gemini
- `deepseek`: DeepSeek API
- `openai`: OpenAI API
---
### 6. system_settings (系統設定表)
儲存系統的各種設定參數。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 設定ID |
| `setting_key` | VARCHAR(100) | UNIQUE, NOT NULL | 設定鍵 |
| `setting_value` | TEXT | NULL | 設定值 |
| `setting_type` | VARCHAR(50) | DEFAULT 'string' | 設定類型 |
| `description` | TEXT | NULL | 說明 |
| `is_public` | BOOLEAN | DEFAULT FALSE | 是否公開(前端可見) |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
| `updated_at` | TIMESTAMP | AUTO UPDATE | 更新時間 |
| `updated_by` | INT | FK, NULL | 更新者ID |
**索引**:
- PRIMARY KEY: `id`
- UNIQUE KEY: `setting_key`
- FOREIGN KEY: `updated_by` REFERENCES `users(id)`
- INDEX: `idx_setting_key`, `idx_is_public`
**設定類型**:
- `string`: 字串
- `number`: 數字
- `boolean`: 布林值
- `json`: JSON 物件
---
### 7. audit_logs (稽核日誌表)
記錄所有重要操作的稽核日誌,用於安全追蹤和問題排查。
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `id` | INT | PK, AUTO_INCREMENT | 日誌ID |
| `user_id` | INT | FK, NULL | 使用者ID |
| `action` | VARCHAR(100) | NOT NULL | 動作類型 |
| `entity_type` | VARCHAR(50) | NULL | 實體類型 |
| `entity_id` | INT | NULL | 實體ID |
| `old_value` | JSON | NULL | 舊值 |
| `new_value` | JSON | NULL | 新值 |
| `ip_address` | VARCHAR(45) | NULL | IP位址 |
| `user_agent` | TEXT | NULL | User Agent |
| `status` | ENUM | DEFAULT 'success' | 執行狀態 |
| `error_message` | TEXT | NULL | 錯誤訊息 |
| `created_at` | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 建立時間 |
**索引**:
- PRIMARY KEY: `id`
- FOREIGN KEY: `user_id` REFERENCES `users(id)`
- INDEX: `idx_user_id`, `idx_action`, `idx_created_at`, `idx_entity`
**常見動作類型**:
- `login`: 登入
- `logout`: 登出
- `create_analysis`: 建立分析
- `update_user`: 更新使用者
- `delete_user`: 刪除使用者
- `update_llm_config`: 更新 LLM 配置
- `update_setting`: 更新系統設定
---
### 8. sessions (Session表)
儲存使用者的 Session 資料(使用 express-session
**欄位說明**:
| 欄位名稱 | 資料型態 | 限制 | 說明 |
|---------|---------|------|------|
| `session_id` | VARCHAR(128) | PK | Session ID |
| `expires` | BIGINT UNSIGNED | NOT NULL | 過期時間Unix timestamp |
| `data` | TEXT | NULL | Session 資料 |
**索引**:
- PRIMARY KEY: `session_id`
- INDEX: `idx_expires`
---
## 關聯圖
```
users (1) ──< (N) analyses
└──< (N) analysis_perspectives
└──< (N) analysis_whys
users (1) ──< (N) llm_configs (created_by)
users (1) ──< (N) system_settings (updated_by)
users (1) ──< (N) audit_logs
```
**關聯說明**:
- 一個使用者可以有多筆分析記錄
- 一筆分析記錄有多個分析角度通常3個
- 一個分析角度有多個 Why 記錄1-5個
- 所有外鍵使用 `ON DELETE CASCADE``ON DELETE SET NULL` 確保資料完整性
---
## 索引策略
### 主要索引
1. **主鍵索引**: 所有資料表的 `id` 欄位
2. **唯一索引**: `users.employee_id`, `users.email`, `system_settings.setting_key`
3. **外鍵索引**: 所有外鍵欄位自動建立索引
### 查詢最佳化索引
1. `analyses.idx_user_id`: 加速查詢使用者的分析記錄
2. `analyses.idx_status`: 加速查詢特定狀態的分析
3. `analyses.idx_created_at`: 加速時間範圍查詢
4. `audit_logs.idx_action`: 加速查詢特定動作的日誌
5. `llm_configs.idx_is_active`: 快速找到啟用的 LLM 配置
---
## 視圖說明
### 1. user_analysis_stats
使用者分析統計視圖,用於管理後台儀表板。
```sql
SELECT
u.id AS user_id,
u.username,
u.employee_id,
u.department,
COUNT(a.id) AS total_analyses,
COUNT(CASE WHEN a.status = 'completed' THEN 1 END) AS completed_analyses,
COUNT(CASE WHEN a.status = 'failed' THEN 1 END) AS failed_analyses,
AVG(a.processing_time) AS avg_processing_time,
MAX(a.created_at) AS last_analysis_at
FROM users u
LEFT JOIN analyses a ON u.id = a.user_id
GROUP BY u.id;
```
### 2. recent_analyses
最近分析記錄視圖,顯示最近 100 筆分析。
```sql
SELECT
a.id,
a.finding,
u.username,
u.employee_id,
a.output_language,
a.status,
a.processing_time,
a.created_at
FROM analyses a
JOIN users u ON a.user_id = u.id
ORDER BY a.created_at DESC
LIMIT 100;
```
---
## 資料字典
### 語言代碼對照表
| 代碼 | 語言 |
|------|------|
| zh-TW | 繁體中文 |
| zh-CN | 简体中文 |
| en | English |
| ja | 日本語 |
| ko | 한국어 |
| vi | Tiếng Việt |
| th | ภาษาไทย |
### 權限等級對照表
| 等級 | 說明 | 權限 |
|------|------|------|
| user | 一般使用者 | 執行分析、查看自己的記錄 |
| admin | 管理者 | 使用者管理、查看所有記錄 |
| super_admin | 最高權限管理者 | 完整系統控制權 |
---
## 維護建議
### 定期維護
1. **清理過期 Sessions**: 每日清理過期的 session 記錄
2. **稽核日誌歸檔**: 每月將舊日誌移至歸檔表
3. **索引最佳化**: 每週執行 `OPTIMIZE TABLE`
4. **備份策略**: 每日全備份,每小時增量備份
### 效能監控
1. 監控慢查詢(> 1秒
2. 監控資料表大小成長
3. 監控索引使用率
4. 監控連線數
---
**文件版本**: 1.0.0
**最後更新**: 2025-12-05
**維護者**: System Administrator

53
docs/user_command_log.md Normal file
View File

@@ -0,0 +1,53 @@
# User Command Log
## Version 1.0.0
### 2025-12-05
#### 初始需求
- 用戶提供了 5 Why 分析器的 React 組件代碼
- 要求整合 Ollama API 替代 Anthropic API
- Ollama API URL: `https://ollama_pjapi.theaken.com`
- 使用模型: `qwen2.5:3b`
#### 執行的開發任務
1. 建立 Node.js + Express 後端服務器
2. 整合 Ollama API (qwen2.5:3b 模型)
3. 建立 React + Vite 前端專案
4. 配置 Tailwind CSS
5. 實現 5 Why 分析功能
6. 實現多語言翻譯功能
#### 用戶要求按照完整開發流程 SOP
- 要求遵循 Phase 0 ~ Phase 9 的完整開發流程
- 包含版本控制、資料庫架構、UI/UX 預覽、管理者功能、資安檢視等
### 待確認事項
1. Gitea Repository 資訊
- 伺服器位址
- Repository 名稱
- 使用者帳號
2. MySQL 資料庫資訊
- 伺服器位址
- Port
- 使用者帳號
- 密碼
- Database 名稱
3. 技術棧確認
- 後端: Node.js (Express) 或 Python (Flask/FastAPI)?
- 前端: React + Vite (已確認)
---
## 變更歷程
### v1.0.0 (2025-12-05)
- 初始專案建立
- 完成 Phase 0: 專案初始化
- ✅ 建立專案資料夾結構
- ✅ 建立 .env.example
- ✅ 建立 .gitignore
- ✅ 更新 package.json (新增安全性相關套件)
- 🔄 建立 README.md (進行中)

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>5 Why 根因分析器</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "5why-analyzer",
"version": "1.0.0",
"description": "5 Why Root Cause Analysis Tool with Ollama API Integration",
"type": "module",
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\"",
"server": "node server.js",
"client": "vite",
"build": "vite build",
"preview": "vite preview",
"db:init": "node scripts/init-database.js",
"db:test": "node scripts/test-db-connection.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"5why",
"root-cause-analysis",
"ollama",
"qwen"
],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"axios": "^1.6.2",
"dotenv": "^16.3.1",
"bcryptjs": "^2.4.3",
"express-session": "^1.17.3",
"express-rate-limit": "^7.1.5",
"mysql2": "^3.6.5",
"helmet": "^7.1.0",
"csv-parser": "^3.0.0",
"json2csv": "^6.0.0-alpha.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8",
"concurrently": "^8.2.2",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.32",
"autoprefixer": "^10.4.16"
}
}

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,75 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import bcrypt from 'bcryptjs';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dbConfig = {
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
multipleStatements: true
};
async function initDatabase() {
let connection;
try {
console.log('\n╔════════════════════════════════════════════╗');
console.log('║ 5 Why Analyzer - Database Initialization ║');
console.log('╚════════════════════════════════════════════╝\n');
console.log('🔄 Connecting...');
connection = await mysql.createConnection(dbConfig);
console.log('✅ Connected\n');
// 生成密碼 hash
const adminPassword = process.env.ADMIN_PASSWORD || 'Admin@123456';
const adminHash = await bcrypt.hash(adminPassword, 10);
// 讀取並執行 SQL
const sqlPath = path.join(__dirname, '../docs/db_schema.sql');
let sqlContent = fs.readFileSync(sqlPath, 'utf8');
sqlContent = sqlContent.replace(/\$2a\$10\$YourBcryptHashHere/g, adminHash);
console.log('📝 Executing SQL statements...\n');
await connection.query(sqlContent);
console.log('\n✅ Database initialized successfully!\n');
// 列出資料表
const [tables] = await connection.execute(
"SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_name LIKE '%' ORDER BY table_name",
[dbConfig.database]
);
const fiveWhyTables = tables.filter(t => !t.table_name.startsWith('hr_') && !t.table_name.startsWith('pm_'));
if (fiveWhyTables.length > 0) {
console.log('📊 5 Why Analyzer Tables:');
fiveWhyTables.forEach((table, index) => {
console.log(` ${index + 1}. ${table.table_name}`);
});
}
console.log(`\n🔐 Admin credentials:`);
console.log(` Email: ${process.env.ADMIN_EMAIL || 'admin@example.com'}`);
console.log(` Password: ${adminPassword}\n`);
} catch (error) {
console.error('\n❌ Error:', error.message);
process.exit(1);
} finally {
if (connection) await connection.end();
}
}
initDatabase();

134
scripts/init-database.js Normal file
View File

@@ -0,0 +1,134 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import bcrypt from 'bcryptjs';
// 載入環境變數
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 資料庫配置
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT) || 33306,
user: process.env.DB_USER || 'A102',
password: process.env.DB_PASSWORD || 'Bb123456',
database: process.env.DB_NAME || 'db_A102',
multipleStatements: true
};
async function initDatabase() {
let connection;
try {
console.log('🔄 Connecting to database...');
console.log(` Host: ${dbConfig.host}:${dbConfig.port}`);
console.log(` Database: ${dbConfig.database}`);
connection = await mysql.createConnection(dbConfig);
console.log('✅ Connected successfully\n');
// 讀取 SQL 檔案
const sqlPath = path.join(__dirname, '../docs/db_schema.sql');
console.log('📖 Reading SQL file:', sqlPath);
let sqlContent = fs.readFileSync(sqlPath, 'utf8');
// 生成管理者密碼 hash
const adminPassword = process.env.ADMIN_PASSWORD || 'Admin@123456';
const adminHash = await bcrypt.hash(adminPassword, 10);
console.log('🔐 Generated admin password hash');
// 替換 SQL 中的密碼 hash
sqlContent = sqlContent.replace(/\$2a\$10\$YourBcryptHashHere/g, adminHash);
// 分割 SQL 語句
const statements = sqlContent
.split(';')
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0 && !stmt.startsWith('--'));
console.log(`\n📝 Found ${statements.length} SQL statements\n`);
// 執行每個語句
for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
// 跳過註解和空白
if (statement.startsWith('--') || statement.trim() === '') {
continue;
}
try {
// 顯示正在執行的語句類型
let action = 'Executing';
if (statement.toUpperCase().includes('CREATE TABLE')) {
const match = statement.match(/CREATE TABLE(?:\s+IF NOT EXISTS)?\s+`?(\w+)`?/i);
if (match) {
action = `Creating table: ${match[1]}`;
}
} else if (statement.toUpperCase().includes('CREATE VIEW')) {
const match = statement.match(/CREATE.*VIEW\s+`?(\w+)`?/i);
if (match) {
action = `Creating view: ${match[1]}`;
}
} else if (statement.toUpperCase().includes('INSERT INTO')) {
const match = statement.match(/INSERT INTO\s+`?(\w+)`?/i);
if (match) {
action = `Inserting data into: ${match[1]}`;
}
}
console.log(` [${i + 1}/${statements.length}] ${action}`);
await connection.execute(statement);
} catch (error) {
// 如果是 "already exists" 錯誤,只顯示警告
if (error.message.includes('already exists')) {
console.log(` ⚠️ Already exists, skipping...`);
} else {
console.error(` ❌ Error: ${error.message}`);
}
}
}
console.log('\n✅ Database initialization completed successfully!\n');
// 顯示建立的資料表
const [tables] = await connection.execute('SHOW TABLES');
console.log('📊 Tables in database:');
tables.forEach((table, index) => {
const tableName = Object.values(table)[0];
console.log(` ${index + 1}. ${tableName}`);
});
console.log('\n🔐 Default admin credentials:');
console.log(` Email: ${process.env.ADMIN_EMAIL || 'admin@example.com'}`);
console.log(` Password: ${adminPassword}`);
console.log('\n⚠ Please change the admin password after first login!\n');
} catch (error) {
console.error('\n❌ Database initialization failed:');
console.error(' Error:', error.message);
if (error.code) {
console.error(' Error Code:', error.code);
}
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('🔌 Database connection closed\n');
}
}
}
// 執行初始化
console.log('\n╔════════════════════════════════════════════╗');
console.log('║ 5 Why Analyzer - Database Initialization ║');
console.log('╚════════════════════════════════════════════╝\n');
initDatabase();

View File

@@ -0,0 +1,89 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
const dbConfig = {
host: process.env.DB_HOST || 'mysql.theaken.com',
port: parseInt(process.env.DB_PORT) || 33306,
user: process.env.DB_USER || 'A102',
password: process.env.DB_PASSWORD || 'Bb123456',
database: process.env.DB_NAME || 'db_A102'
};
async function testConnection() {
let connection;
try {
console.log('\n╔═══════════════════════════════════════╗');
console.log('║ Database Connection Test ║');
console.log('╚═══════════════════════════════════════╝\n');
console.log('📡 Attempting to connect to database...');
console.log(` Host: ${dbConfig.host}`);
console.log(` Port: ${dbConfig.port}`);
console.log(` Database: ${dbConfig.database}`);
console.log(` User: ${dbConfig.user}\n`);
connection = await mysql.createConnection(dbConfig);
console.log('✅ Connection successful!\n');
// 測試基本查詢
console.log('🔍 Testing basic queries...\n');
// 1. 檢查資料庫版本
const [versionResult] = await connection.execute('SELECT VERSION() as version');
console.log(` MySQL Version: ${versionResult[0].version}`);
// 2. 列出所有資料表
const [tables] = await connection.execute('SHOW TABLES');
console.log(` Tables found: ${tables.length}`);
if (tables.length > 0) {
console.log('\n📊 Available tables:');
tables.forEach((table, index) => {
const tableName = Object.values(table)[0];
console.log(` ${index + 1}. ${tableName}`);
});
// 3. 檢查每個資料表的記錄數
console.log('\n📈 Table statistics:');
for (const table of tables) {
const tableName = Object.values(table)[0];
const [countResult] = await connection.execute(`SELECT COUNT(*) as count FROM \`${tableName}\``);
console.log(` ${tableName}: ${countResult[0].count} rows`);
}
} else {
console.log('\n⚠ No tables found. Run "npm run db:init" to initialize the database.');
}
console.log('\n✅ All tests passed!\n');
} catch (error) {
console.error('\n❌ Connection failed!');
console.error(` Error: ${error.message}`);
if (error.code) {
console.error(` Error Code: ${error.code}`);
}
if (error.errno) {
console.error(` Error Number: ${error.errno}`);
}
console.log('\n💡 Troubleshooting tips:');
console.log(' 1. Check if MySQL server is running');
console.log(' 2. Verify host and port in .env file');
console.log(' 3. Confirm database credentials');
console.log(' 4. Check firewall settings');
console.log(' 5. Ensure database exists\n');
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('🔌 Connection closed\n');
}
}
}
testConnection();

155
server.js Normal file
View File

@@ -0,0 +1,155 @@
import express from 'express';
import cors from 'cors';
import axios from 'axios';
const app = express();
const PORT = 3001;
// Ollama API 設定
const OLLAMA_API_URL = "https://ollama_pjapi.theaken.com";
const MODEL_NAME = "qwen2.5:3b"; // 使用 qwen2.5:3b 模型
app.use(cors());
app.use(express.json());
// 健康檢查端點
app.get('/health', (req, res) => {
res.json({ status: 'ok', message: 'Server is running' });
});
// 列出可用模型
app.get('/api/models', async (req, res) => {
try {
const response = await axios.get(`${OLLAMA_API_URL}/v1/models`);
res.json(response.data);
} catch (error) {
console.error('Error fetching models:', error.message);
res.status(500).json({ error: 'Failed to fetch models', details: error.message });
}
});
// 5 Why 分析端點
app.post('/api/analyze', async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
try {
console.log('Sending request to Ollama API...');
const chatRequest = {
model: MODEL_NAME,
messages: [
{
role: "system",
content: "You are an expert consultant specializing in 5 Why root cause analysis. You always respond in valid JSON format without any markdown code blocks."
},
{
role: "user",
content: prompt
}
],
temperature: 0.7,
stream: false
};
const response = await axios.post(
`${OLLAMA_API_URL}/v1/chat/completions`,
chatRequest,
{
headers: {
'Content-Type': 'application/json'
},
timeout: 120000 // 120 seconds timeout
}
);
if (response.data && response.data.choices && response.data.choices[0]) {
const content = response.data.choices[0].message.content;
console.log('Received response from Ollama');
res.json({ content });
} else {
throw new Error('Invalid response format from Ollama API');
}
} catch (error) {
console.error('Error calling Ollama API:', error.message);
if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
}
res.status(500).json({
error: 'Failed to analyze with Ollama API',
details: error.message,
responseData: error.response?.data
});
}
});
// 翻譯端點
app.post('/api/translate', async (req, res) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
try {
console.log('Translating with Ollama API...');
const chatRequest = {
model: MODEL_NAME,
messages: [
{
role: "system",
content: "You are a professional translator. You always respond in valid JSON format without any markdown code blocks."
},
{
role: "user",
content: prompt
}
],
temperature: 0.3,
stream: false
};
const response = await axios.post(
`${OLLAMA_API_URL}/v1/chat/completions`,
chatRequest,
{
headers: {
'Content-Type': 'application/json'
},
timeout: 120000
}
);
if (response.data && response.data.choices && response.data.choices[0]) {
const content = response.data.choices[0].message.content;
console.log('Translation completed');
res.json({ content });
} else {
throw new Error('Invalid response format from Ollama API');
}
} catch (error) {
console.error('Error translating with Ollama API:', error.message);
if (error.response) {
console.error('Response data:', error.response.data);
console.error('Response status:', error.response.status);
}
res.status(500).json({
error: 'Failed to translate with Ollama API',
details: error.message,
responseData: error.response?.data
});
}
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
console.log(`Ollama API URL: ${OLLAMA_API_URL}`);
console.log(`Using model: ${MODEL_NAME}`);
});

7
src/App.jsx Normal file
View File

@@ -0,0 +1,7 @@
import FiveWhyAnalyzer from './FiveWhyAnalyzer'
function App() {
return <FiveWhyAnalyzer />
}
export default App

547
src/FiveWhyAnalyzer.jsx Normal file
View File

@@ -0,0 +1,547 @@
import { useState } from "react";
export default function FiveWhyAnalyzer() {
const [finding, setFinding] = useState("");
const [jobContent, setJobContent] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [translating, setTranslating] = useState(false);
const [error, setError] = useState("");
const [outputLanguage, setOutputLanguage] = useState("zh-TW");
const [currentLanguage, setCurrentLanguage] = useState("zh-TW");
const [showGuide, setShowGuide] = useState(false);
const API_BASE_URL = "http://localhost:3001";
const languages = [
{ code: "zh-TW", name: "繁體中文", flag: "🇹🇼" },
{ code: "zh-CN", name: "简体中文", flag: "🇨🇳" },
{ code: "en", name: "English", flag: "🇺🇸" },
{ code: "ja", name: "日本語", flag: "🇯🇵" },
{ code: "ko", name: "한국어", flag: "🇰🇷" },
{ code: "vi", name: "Tiếng Việt", flag: "🇻🇳" },
{ code: "th", name: "ภาษาไทย", flag: "🇹🇭" },
];
const guidelines = [
{
title: "精準定義問題",
subtitle: "描述現象,而非結論",
icon: "🎯",
color: "bg-rose-50 border-rose-200",
content: "起點若是錯誤後續分析皆是枉然。必須客觀描述「發生了什麼事」包含人、事、時、地、物5W1H。",
example: { bad: "機器壞了", good: "A 機台在下午 2 點運轉時,主軸過熱導致停機" }
},
{
title: "聚焦流程與系統",
subtitle: "而非責備個人",
icon: "⚙️",
color: "bg-amber-50 border-amber-200",
content: "若分析導向「某人不小心」或「某人忘記了」,這不是根本原因。人本來就會犯錯,應追問:「為什麼系統允許這個疏失發生?」",
principle: "解決問題的機制,而非責備犯錯的人"
},
{
title: "基於事實與現場",
subtitle: "拒絕猜測",
icon: "🔍",
color: "bg-emerald-50 border-emerald-200",
content: "每一個「為什麼」的回答都必須是經過查證的事實,不能是「我覺得應該是...」或「可能是...」。",
principle: "三現主義:現場、現物、現實"
},
{
title: "邏輯雙向檢核",
subtitle: "因果關係必須嚴謹",
icon: "🔄",
color: "bg-blue-50 border-blue-200",
content: "順向:若原因 X 發生,是否必然導致結果 Y逆向若消除原因 X結果 Y 是否就不會發生?",
principle: "若無法雙向通過,代表邏輯有斷層"
},
{
title: "止於可執行對策",
subtitle: "永久性對策,非暫時性",
icon: "✅",
color: "bg-violet-50 border-violet-200",
content: "當追問到可以透過具體行動來根除的層次時,就是停止追問的時刻。對策必須是「永久性」的,而非「重新訓練、加強宣導」等暫時性措施。",
principle: "目的是解決問題,不是寫報告"
}
];
const getLanguageName = (code) => {
const lang = languages.find((l) => l.code === code);
return lang ? lang.name : code;
};
const analyzeWith5Why = async () => {
if (!finding.trim() || !jobContent.trim()) {
setError("請填寫所有欄位");
return;
}
setLoading(true);
setError("");
setResults([]);
const langName = getLanguageName(outputLanguage);
const prompt = `你是一位專精於「5 Why 根因分析法」的資深顧問。請嚴格遵循以下五大執行要項進行分析:
## 五大執行要項
### 1. 精準定義問題(描述現象,而非結論)
- 第一步必須客觀描述「發生了什麼事」,而非直接跳入「我認為是甚麼問題」
- 具體化包含人、事、時、地、物5W1H
### 2. 聚焦於「流程」與「系統」,而非「人」
- 若答案是「人為疏失」,請繼續追問:「為什麼系統允許這個疏失發生?」
- 原則:解決問題的機制,而非責備犯錯的人
### 3. 基於「事實」與「現場」,拒絕「猜測」
- 每一個「為什麼」的回答,都必須是可查證的事實
- 若無法確認,應標註需要驗證的假設
### 4. 邏輯的「雙向檢核」
- 順向檢查:若原因 X 發生,是否必然導致結果 Y
- 逆向檢查:若消除了原因 X結果 Y 是否就不會發生?
### 5. 止於「可執行的對策」
- 根本原因必須能對應到一個「永久性對策」(不再發生)
- 不僅是「暫時性對策」(如:重新訓練、加強宣導)
---
## 待分析內容
**Finding發現的問題/現象):** ${finding}
**工作內容背景:** ${jobContent}
---
## 輸出要求
請提供 **三個不同角度** 的 5 Why 分析,每個分析從不同的切入點出發(例如:流程面、系統面、管理面、設備面、環境面等)。
注意:
- 5 Why 的目的不是「湊滿五個問題」,而是穿透表面症狀直達根本原因
- 若在第 3 或第 4 個 Why 就已找到真正的根本原因,可以停止(設為 null
- 每個 Why 必須標註是「已驗證事實」還是「待驗證假設」
- 最終對策必須是「永久性對策」
⚠️ 重要:請使用 **${langName}** 語言回覆所有內容。
請用以下 JSON 格式回覆(不要加任何 markdown 標記):
{
"problemRestatement": "根據 5W1H 重新描述的問題定義",
"analyses": [
{
"perspective": "分析角度(如:流程面)",
"perspectiveIcon": "適合的 emoji",
"whys": [
{
"level": 1,
"question": "為什麼...?",
"answer": "因為...",
"isVerified": true,
"verificationNote": "已確認/需驗證:說明"
}
],
"rootCause": "根本原因(系統/流程層面)",
"logicCheck": {
"forward": "順向檢核:如果[原因]發生,則[結果]必然發生",
"backward": "逆向檢核:如果消除[原因],則[結果]不會發生",
"isValid": true
},
"countermeasure": {
"permanent": "永久性對策(系統性解決方案)",
"actionItems": ["具體行動項目1", "具體行動項目2"],
"avoidList": ["避免的暫時性做法(如:加強宣導)"]
}
}
]
}`;
try {
const response = await fetch(`${API_BASE_URL}/api/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || data.details || "分析失敗");
}
const text = data.content;
const clean = text.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(clean);
setResults(parsed);
setCurrentLanguage(outputLanguage);
} catch (err) {
console.error("Analysis error:", err);
setError("分析失敗,請稍後再試:" + err.message);
} finally {
setLoading(false);
}
};
const translateResults = async (targetLang) => {
if (!results.analyses || targetLang === currentLanguage) return;
setTranslating(true);
setError("");
const langName = getLanguageName(targetLang);
const prompt = `請將以下 5 Why 分析結果翻譯成 **${langName}**。
原始內容:
${JSON.stringify(results, null, 2)}
請保持完全相同的 JSON 結構,只翻譯文字內容。
請用以下 JSON 格式回覆(不要加任何 markdown 標記):
{
"problemRestatement": "...",
"analyses": [...]
}`;
try {
const response = await fetch(`${API_BASE_URL}/api/translate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || data.details || "翻譯失敗");
}
const text = data.content;
const clean = text.replace(/```json|```/g, "").trim();
const parsed = JSON.parse(clean);
setResults(parsed);
setCurrentLanguage(targetLang);
} catch (err) {
console.error("Translation error:", err);
setError("翻譯失敗:" + err.message);
} finally {
setTranslating(false);
}
};
const cardColors = [
{ header: "bg-blue-500", headerText: "text-white", border: "border-blue-200" },
{ header: "bg-violet-500", headerText: "text-white", border: "border-violet-200" },
{ header: "bg-teal-500", headerText: "text-white", border: "border-teal-200" },
];
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 p-4 md:p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-6">
<h1 className="text-3xl md:text-4xl font-bold text-slate-800 mb-2">
🔍 5 Why 根因分析器
</h1>
<p className="text-slate-500 text-sm md:text-base">
穿透問題表面直達根本原因產出永久性對策
</p>
<p className="text-xs text-slate-400 mt-1">
Powered by Ollama API (qwen2.5:3b)
</p>
</div>
{/* Guidelines Toggle */}
<div className="mb-6">
<button
onClick={() => setShowGuide(!showGuide)}
className="w-full py-3 bg-white hover:bg-slate-50 border border-slate-200 rounded-xl text-slate-600 font-medium transition-all flex items-center justify-center gap-2 shadow-sm"
>
📚 5 Why 執行要項指南
<span className={`transition-transform ${showGuide ? "rotate-180" : ""}`}></span>
</button>
{showGuide && (
<div className="mt-4 grid md:grid-cols-2 lg:grid-cols-3 gap-4">
{guidelines.map((guide, idx) => (
<div key={idx} className={`${guide.color} rounded-xl p-4 border shadow-sm`}>
<div className="flex items-start gap-3 mb-3">
<span className="text-2xl">{guide.icon}</span>
<div>
<h3 className="text-slate-800 font-bold">{guide.title}</h3>
<p className="text-slate-500 text-sm">{guide.subtitle}</p>
</div>
</div>
<p className="text-slate-600 text-sm mb-3">{guide.content}</p>
{guide.example && (
<div className="bg-white/70 rounded-lg p-3 text-xs">
<div className="text-red-600 mb-1"> {guide.example.bad}</div>
<div className="text-emerald-600"> {guide.example.good}</div>
</div>
)}
{guide.principle && (
<div className="bg-white/70 rounded-lg p-2 text-xs text-blue-700 border border-blue-200">
💡 {guide.principle}
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Input Section */}
<div className="bg-white rounded-2xl p-6 mb-6 border border-slate-200 shadow-sm">
<div className="grid md:grid-cols-2 gap-6 mb-6">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
📋 我負責的 Finding 是什麼
</label>
<p className="text-xs text-slate-400 mb-2">
請具體描述現象5W1H何人何事何時何地如何發生
</p>
<textarea
value={finding}
onChange={(e) => setFinding(e.target.value)}
placeholder="範例A 機台在 12/5 下午 2 點運轉時,主軸溫度達 95°C 觸發過熱保護導致停機,影響當日產能 200 件..."
className="w-full h-36 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
💼 我的工作內容是什麼
</label>
<p className="text-xs text-slate-400 mb-2">
說明您的職責範圍幫助分析聚焦在可控因素
</p>
<textarea
value={jobContent}
onChange={(e) => setJobContent(e.target.value)}
placeholder="範例:負責生產線設備維護與品質管控,管理 5 台 CNC 機台,需確保 OEE 達 85% 以上..."
className="w-full h-36 px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none text-sm"
/>
</div>
</div>
{/* Language Selection */}
<div className="mb-6">
<label className="block text-sm font-medium text-slate-700 mb-2">
🌐 輸出語言
</label>
<div className="flex flex-wrap gap-2">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => setOutputLanguage(lang.code)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
outputLanguage === lang.code
? "bg-blue-500 text-white shadow-md"
: "bg-slate-100 text-slate-600 hover:bg-slate-200"
}`}
>
{lang.flag} {lang.name}
</button>
))}
</div>
</div>
<button
onClick={analyzeWith5Why}
disabled={loading}
className="w-full py-4 bg-gradient-to-r from-blue-500 to-indigo-500 hover:from-blue-600 hover:to-indigo-600 disabled:from-slate-300 disabled:to-slate-300 text-white font-bold rounded-xl transition-all duration-300 transform hover:scale-[1.02] disabled:scale-100 disabled:cursor-not-allowed shadow-lg"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
深度分析中...
</span>
) : (
"🎯 Find My 5 Why"
)}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-xl text-red-600 text-sm">
{error}
</div>
)}
</div>
{/* Problem Restatement */}
{results.problemRestatement && (
<div className="bg-indigo-50 rounded-2xl p-5 mb-6 border border-indigo-200 shadow-sm">
<h3 className="text-indigo-700 font-bold mb-2 flex items-center gap-2">
📝 問題重新定義5W1H
</h3>
<p className="text-indigo-900">{results.problemRestatement}</p>
</div>
)}
{/* Translation Bar */}
{results.analyses && results.analyses.length > 0 && (
<div className="bg-white rounded-xl p-4 mb-6 border border-slate-200 shadow-sm">
<div className="flex flex-wrap items-center gap-3">
<span className="text-slate-600 text-sm font-medium">🔄 翻譯</span>
<span className="text-slate-500 text-sm">
目前
<span className="text-blue-600 font-medium ml-1">
{languages.find((l) => l.code === currentLanguage)?.flag} {getLanguageName(currentLanguage)}
</span>
</span>
<div className="flex-1" />
<div className="flex flex-wrap gap-2">
{languages.filter((l) => l.code !== currentLanguage).map((lang) => (
<button
key={lang.code}
onClick={() => translateResults(lang.code)}
disabled={translating}
className="px-3 py-1.5 bg-slate-100 hover:bg-slate-200 disabled:bg-slate-50 text-slate-600 text-sm rounded-lg transition-all disabled:cursor-not-allowed"
>
{lang.flag} {lang.name}
</button>
))}
</div>
{translating && (
<span className="flex items-center gap-2 text-blue-500 text-sm">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
翻譯中...
</span>
)}
</div>
</div>
)}
{/* Results Section */}
{results.analyses && results.analyses.length > 0 && (
<div className="grid lg:grid-cols-3 gap-6">
{results.analyses.map((analysis, idx) => (
<div key={idx} className={`bg-white rounded-2xl border ${cardColors[idx].border} overflow-hidden shadow-sm`}>
{/* Card Header */}
<div className={`p-4 ${cardColors[idx].header}`}>
<h3 className={`text-lg font-bold ${cardColors[idx].headerText} flex items-center gap-2`}>
<span className="text-2xl">{analysis.perspectiveIcon || "📊"}</span>
{analysis.perspective}
</h3>
</div>
{/* 5 Whys */}
<div className="p-4 space-y-3">
{analysis.whys.filter(w => w !== null).map((why, wIdx) => (
<div
key={wIdx}
className="bg-slate-50 rounded-lg p-3 border-l-4"
style={{ borderLeftColor: `hsl(${200 + wIdx * 30}, 70%, 50%)` }}
>
<div className="flex items-start gap-2">
<div className="flex flex-col items-center">
<span className="bg-slate-200 text-xs px-2 py-1 rounded font-mono text-slate-600">
W{why.level}
</span>
<span className={`mt-1 text-xs ${why.isVerified ? "text-emerald-500" : "text-amber-500"}`}>
{why.isVerified ? "✓" : "?"}
</span>
</div>
<div className="flex-1">
<p className="text-slate-700 text-sm font-medium mb-1">{why.question}</p>
<p className="text-slate-500 text-sm">{why.answer}</p>
{why.verificationNote && (
<p className={`text-xs mt-1 ${why.isVerified ? "text-emerald-500" : "text-amber-500"}`}>
{why.verificationNote}
</p>
)}
</div>
</div>
</div>
))}
{/* Root Cause */}
<div className="mt-4 p-4 bg-amber-50 rounded-xl border border-amber-200">
<h4 className="text-amber-700 font-bold text-sm mb-2">🎯 根本原因</h4>
<p className="text-amber-900 text-sm">{analysis.rootCause}</p>
</div>
{/* Logic Check */}
{analysis.logicCheck && (
<div className="p-4 bg-slate-50 rounded-xl border border-slate-200">
<h4 className="text-slate-700 font-bold text-sm mb-2 flex items-center gap-2">
🔄 邏輯雙向檢核
<span className={`text-xs px-2 py-0.5 rounded text-white ${analysis.logicCheck.isValid ? "bg-emerald-500" : "bg-red-500"}`}>
{analysis.logicCheck.isValid ? "通過" : "需檢視"}
</span>
</h4>
<div className="space-y-2 text-xs">
<div className="text-slate-600">
<span className="text-blue-600 font-medium">順向</span> {analysis.logicCheck.forward}
</div>
<div className="text-slate-600">
<span className="text-violet-600 font-medium">逆向</span> {analysis.logicCheck.backward}
</div>
</div>
</div>
)}
{/* Countermeasure */}
{analysis.countermeasure && (
<div className="p-4 bg-emerald-50 rounded-xl border border-emerald-200">
<h4 className="text-emerald-700 font-bold text-sm mb-2"> 永久性對策</h4>
<p className="text-emerald-900 text-sm mb-3">{analysis.countermeasure.permanent}</p>
{analysis.countermeasure.actionItems && (
<div className="mb-3">
<p className="text-emerald-700 text-xs font-medium mb-1">行動項目</p>
<ul className="space-y-1">
{analysis.countermeasure.actionItems.map((item, i) => (
<li key={i} className="text-emerald-800 text-xs flex items-start gap-1">
<span></span> {item}
</li>
))}
</ul>
</div>
)}
{analysis.countermeasure.avoidList && analysis.countermeasure.avoidList.length > 0 && (
<div className="bg-red-50 rounded-lg p-2 border border-red-200">
<p className="text-red-600 text-xs font-medium mb-1"> 避免暫時性做法</p>
<ul className="space-y-1">
{analysis.countermeasure.avoidList.map((item, i) => (
<li key={i} className="text-red-500 text-xs flex items-start gap-1">
<span></span> {item}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
</div>
))}
</div>
)}
{/* Empty State */}
{(!results.analyses || results.analyses.length === 0) && !loading && (
<div className="text-center py-16 text-slate-400">
<div className="text-6xl mb-4">🤔</div>
<p className="mb-2 text-slate-500">輸入問題現象與工作內容</p>
<p className="text-sm">系統將依據 5 Why 方法論進行深度根因分析</p>
</div>
)}
{/* Footer */}
<div className="mt-8 text-center text-slate-400 text-xs">
💡 5 Why 的目的不是湊滿五個問題而是穿透表面症狀直達根本原因
</div>
</div>
</div>
);
}

17
src/index.css Normal file
View File

@@ -0,0 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

10
vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true
}
})