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:
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>,
|
||||
)
|
||||
Reference in New Issue
Block a user