import { useEffect, useRef, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { CheckCircle, ChevronRight } from "lucide-react"; import { open } from "@tauri-apps/plugin-dialog"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { ChatWindow } from "@/components/ChatWindow"; import { TriageProgress } from "@/components/TriageProgress"; import { useSessionStore } from "@/stores/sessionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { chatMessageCmd, getIssueCmd, getIssueMessagesCmd, uploadLogFileCmd, updateIssueCmd, addFiveWhyCmd, } from "@/lib/tauriCommands"; import type { TriageMessage } from "@/lib/tauriCommands"; const CLOSE_PATTERNS = [ "close this issue", "please close", "mark as resolved", "mark resolved", "issue is fixed", "issue is resolved", "resolve this", "this is resolved", ]; function isCloseIntent(message: string): boolean { const lower = message.toLowerCase(); return CLOSE_PATTERNS.some((p) => lower.includes(p)); } type PendingFile = { name: string; content: string | null }; export default function Triage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [pendingFiles, setPendingFiles] = useState([]); // Track the last user message so we can save it as a resolution step when why level advances const lastUserMsgRef = useRef(""); const initialized = useRef(false); const { currentIssue, messages, currentWhyLevel, startSession, addMessage, setWhyLevel } = useSessionStore(); const { getActiveProvider } = useSettingsStore(); useEffect(() => { if (!id || initialized.current) return; initialized.current = true; Promise.all([getIssueCmd(id), getIssueMessagesCmd(id)]) .then(([detail, pastMessages]) => { startSession(detail.issue); if (pastMessages.length > 0) { // Restore conversation history from DB pastMessages.forEach((m, i) => { addMessage({ id: `hist-${i}`, issue_id: id, role: m.role, content: m.content, why_level: 0, created_at: Date.now() - (pastMessages.length - i) * 1000, }); }); } else if (detail.resolution_steps.length === 0) { // Fresh issue — show welcome prompt addMessage({ id: "welcome", issue_id: id, role: "assistant", content: `I'll guide you through a 5-Whys root cause analysis for: **"${detail.issue.title}"**\n\nDomain: **${detail.issue.category || "General"}**\n\nDescribe the symptoms you're observing — error messages, affected services, and when the issue started.`, why_level: 0, created_at: Date.now(), }); } }) .catch((e) => setError(String(e))); }, [id]); const handleAttach = async () => { if (!id) return; try { const selected = await open({ multiple: true, filters: [ { name: "Log & Text Files", extensions: ["log", "txt", "json", "xml", "yaml", "yml", "csv", "out", "err"] }, { name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"] }, { name: "All Files", extensions: ["*"] }, ], }); if (!selected) return; const paths = Array.isArray(selected) ? selected : [selected]; for (const filePath of paths) { const logFile = await uploadLogFileCmd(id, filePath); let content: string | null = null; try { const raw = await readTextFile(filePath); content = raw.slice(0, 8000); // cap at 8 KB to keep context manageable } catch { // Binary file (image) — include filename only as context } setPendingFiles((prev) => [...prev, { name: logFile.file_name, content }]); } } catch (e) { setError(`Attachment failed: ${String(e)}`); } }; const handleSend = async (message: string) => { if (!id) return; // Close intent: works regardless of whether issue is fully loaded in session. // Save the user's reason as a resolution step so the Resolution page is never empty. if (isCloseIntent(message) && pendingFiles.length === 0) { try { await addFiveWhyCmd(id, 1, "Resolution", message, "Self-resolved by user"); await updateIssueCmd(id, { status: "resolved" }); navigate("/"); } catch (e) { setError(String(e)); } return; } if (!currentIssue) return; const provider = getActiveProvider(); if (!provider) { setError("No AI provider configured. Go to Settings > AI Providers."); return; } setIsLoading(true); setError(null); // Build AI context: user text + file contents; show only text + filenames in chat UI const fileContext = pendingFiles .map((f) => f.content ? `\n\n--- Attached: ${f.name} ---\n${f.content}` : `\n\n[Image attached: ${f.name} — describe what you see if relevant]` ) .join(""); const aiMessage = pendingFiles.length > 0 ? `${message}${fileContext}` : message; const displayContent = pendingFiles.length > 0 ? `${message}${message ? "\n" : ""}📎 ${pendingFiles.map((f) => f.name).join(", ")}` : message; const userMsg: TriageMessage = { id: `user-${Date.now()}`, issue_id: id, role: "user", content: displayContent, why_level: currentWhyLevel, created_at: Date.now(), }; lastUserMsgRef.current = message; addMessage(userMsg); setPendingFiles([]); try { const response = await chatMessageCmd(id, aiMessage, provider); const assistantMsg: TriageMessage = { id: `asst-${Date.now()}`, issue_id: id, role: "assistant", content: response.content, why_level: currentWhyLevel, created_at: Date.now(), }; addMessage(assistantMsg); const lower = response.content.toLowerCase(); let nextLevel = currentWhyLevel; if (lower.includes("why 2") || (currentWhyLevel === 1 && lower.includes("why is that"))) nextLevel = 2; else if (lower.includes("why 3")) nextLevel = 3; else if (lower.includes("why 4")) nextLevel = 4; else if (lower.includes("why 5")) nextLevel = 5; if (lower.includes("root cause") && (lower.includes("identified") || lower.includes("the root cause is"))) nextLevel = 6; // Auto-save the completed why step as a resolution step if (nextLevel > currentWhyLevel && currentWhyLevel >= 1 && currentWhyLevel <= 5) { addFiveWhyCmd( id, currentWhyLevel, `Why ${currentWhyLevel}: ${lastUserMsgRef.current}`, response.content.slice(0, 500), "" ).catch(() => {}); // non-blocking, best-effort } if (nextLevel !== currentWhyLevel) setWhyLevel(nextLevel); } catch (e) { setError(String(e)); } finally { setIsLoading(false); } }; const canProceed = currentWhyLevel >= 5 || messages.some( (m) => m.role === "assistant" && m.content.toLowerCase().includes("root cause") ); return (

{currentIssue?.title ?? "Loading..."}

5-Whys Root Cause Analysis

{error && (
{error}
)}
setPendingFiles((prev) => prev.filter((_, idx) => idx !== i))} />
); }