Initial commit: 5 Why Root Cause Analyzer v1.0.0

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

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

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

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

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

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