Files
5why-analyzer/5why-analyzer (1).jsx
donald 78efac64e2 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>
2025-12-05 18:29:29 +08:00

545 lines
24 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}