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:
15
.claude/settings.local.json
Normal file
15
.claude/settings.local.json
Normal 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
35
.env.example
Normal 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
79
.gitignore
vendored
Normal 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
544
5why-analyzer (1).jsx
Normal 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
122
README.md
Normal 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
343
README_FULL.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# 5 Why 根因分析器 (5 Why Root Cause Analyzer)
|
||||||
|
|
||||||
|
[](https://github.com/yourusername/5why-analyzer)
|
||||||
|
[](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
102
config.js
Normal 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
229
docs/CHANGELOG.md
Normal 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
416
docs/db_schema.md
Normal 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
53
docs/user_command_log.md
Normal 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
13
index.html
Normal 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
47
package.json
Normal 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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
75
scripts/init-database-simple.js
Normal file
75
scripts/init-database-simple.js
Normal 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
134
scripts/init-database.js
Normal 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();
|
||||||
89
scripts/test-db-connection.js
Normal file
89
scripts/test-db-connection.js
Normal 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
155
server.js
Normal 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
7
src/App.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import FiveWhyAnalyzer from './FiveWhyAnalyzer'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <FiveWhyAnalyzer />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
547
src/FiveWhyAnalyzer.jsx
Normal file
547
src/FiveWhyAnalyzer.jsx
Normal 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
17
src/index.css
Normal 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
10
src/main.jsx
Normal 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
11
tailwind.config.js
Normal 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
10
vite.config.js
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user