From 4b02ca1004a63b2289103d64fb9e4944980a393e Mon Sep 17 00:00:00 2001 From: Sehmuel Wagner <111253615+sachmii@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:50:53 +0200 Subject: [PATCH 1/7] Extract icons into seperate files --- client/src/App.jsx | 80 +------------------ client/src/components/PrivacyToggle.jsx | 42 +--------- client/src/components/icons/AnalyzeIcon.jsx | 19 +++++ client/src/components/icons/ChevronIcon.jsx | 19 +++++ client/src/components/icons/CloudIcon.jsx | 19 +++++ .../components/icons/EmptyInsightsIcon.jsx | 32 ++++++++ client/src/components/icons/EmptyLogsIcon.jsx | 29 +++++++ client/src/components/icons/IngestIcon.jsx | 33 ++++++++ client/src/components/icons/LocalIcon.jsx | 30 +++++++ client/src/components/icons/index.js | 7 ++ 10 files changed, 195 insertions(+), 115 deletions(-) create mode 100644 client/src/components/icons/AnalyzeIcon.jsx create mode 100644 client/src/components/icons/ChevronIcon.jsx create mode 100644 client/src/components/icons/CloudIcon.jsx create mode 100644 client/src/components/icons/EmptyInsightsIcon.jsx create mode 100644 client/src/components/icons/EmptyLogsIcon.jsx create mode 100644 client/src/components/icons/IngestIcon.jsx create mode 100644 client/src/components/icons/LocalIcon.jsx create mode 100644 client/src/components/icons/index.js diff --git a/client/src/App.jsx b/client/src/App.jsx index facd4dd..2865021 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,6 +4,7 @@ import PrivacyToggle from "@/components/PrivacyToggle"; import IngestModal from "@/components/IngestModal"; import LogList from "@/components/LogList"; import InsightsPanel from "@/components/InsightsPanel"; +import { IngestIcon, EmptyLogsIcon, EmptyInsightsIcon } from "@/components/icons"; import { usePrivacyMode } from "@/context/PrivacyModeContext"; import { ingestLog, analyzeLog } from "@/lib/api"; @@ -79,34 +80,7 @@ function App() { id="btn-ingest" onClick={() => setShowIngestModal(true)} > - + Ingest Logs @@ -129,30 +103,7 @@ function App() { ) : (
- +

No Logs Ingested

@@ -63,20 +40,7 @@ export default function PrivacyToggle() { onClick={() => setMode("cloud")} id="privacy-toggle-cloud" > - + Cloud Expert
diff --git a/client/src/components/icons/AnalyzeIcon.jsx b/client/src/components/icons/AnalyzeIcon.jsx new file mode 100644 index 0000000..97a8994 --- /dev/null +++ b/client/src/components/icons/AnalyzeIcon.jsx @@ -0,0 +1,19 @@ +export default function AnalyzeIcon({ className = "ghost-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/ChevronIcon.jsx b/client/src/components/icons/ChevronIcon.jsx new file mode 100644 index 0000000..360e310 --- /dev/null +++ b/client/src/components/icons/ChevronIcon.jsx @@ -0,0 +1,19 @@ +export default function ChevronIcon({ className = "chevron-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/CloudIcon.jsx b/client/src/components/icons/CloudIcon.jsx new file mode 100644 index 0000000..e79cc84 --- /dev/null +++ b/client/src/components/icons/CloudIcon.jsx @@ -0,0 +1,19 @@ +export default function CloudIcon({ className = "toggle-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/EmptyInsightsIcon.jsx b/client/src/components/icons/EmptyInsightsIcon.jsx new file mode 100644 index 0000000..b987da3 --- /dev/null +++ b/client/src/components/icons/EmptyInsightsIcon.jsx @@ -0,0 +1,32 @@ +export default function EmptyInsightsIcon({ + className = "empty-icon", + ...props +}) { + return ( + + ); +} diff --git a/client/src/components/icons/EmptyLogsIcon.jsx b/client/src/components/icons/EmptyLogsIcon.jsx new file mode 100644 index 0000000..cbf4916 --- /dev/null +++ b/client/src/components/icons/EmptyLogsIcon.jsx @@ -0,0 +1,29 @@ +export default function EmptyLogsIcon({ className = "empty-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/IngestIcon.jsx b/client/src/components/icons/IngestIcon.jsx new file mode 100644 index 0000000..a9bc399 --- /dev/null +++ b/client/src/components/icons/IngestIcon.jsx @@ -0,0 +1,33 @@ +export default function IngestIcon({ className = "ghost-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/LocalIcon.jsx b/client/src/components/icons/LocalIcon.jsx new file mode 100644 index 0000000..8afd589 --- /dev/null +++ b/client/src/components/icons/LocalIcon.jsx @@ -0,0 +1,30 @@ +export default function LocalIcon({ className = "toggle-icon", ...props }) { + return ( + + ); +} diff --git a/client/src/components/icons/index.js b/client/src/components/icons/index.js new file mode 100644 index 0000000..18f6eda --- /dev/null +++ b/client/src/components/icons/index.js @@ -0,0 +1,7 @@ +export { default as IngestIcon } from "./IngestIcon"; +export { default as EmptyLogsIcon } from "./EmptyLogsIcon"; +export { default as EmptyInsightsIcon } from "./EmptyInsightsIcon"; +export { default as AnalyzeIcon } from "./AnalyzeIcon"; +export { default as LocalIcon } from "./LocalIcon"; +export { default as CloudIcon } from "./CloudIcon"; +export { default as ChevronIcon } from "./ChevronIcon"; From eaca40cc2dfab4cb3ca1b5f4ea050d9232aa3b56 Mon Sep 17 00:00:00 2001 From: Sehmuel Wagner <111253615+sachmii@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:52:34 +0200 Subject: [PATCH 2/7] fix log list --- client/src/components/LogList.css | 44 ++++++++++++++++++++++++- client/src/components/LogList.jsx | 54 ++++++++++++++++++------------- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/client/src/components/LogList.css b/client/src/components/LogList.css index e97c870..d543326 100644 --- a/client/src/components/LogList.css +++ b/client/src/components/LogList.css @@ -67,6 +67,41 @@ margin-left: auto; } +/* ─── Expand / Collapse Button ───────────────────────── */ + +.expand-btn { + display: inline-flex; + align-items: center; + justify-content: center; + background: none; + border: 1px solid #2a3a6a; + color: #5a7aa3; + width: 22px; + height: 22px; + padding: 0; + cursor: pointer; + flex-shrink: 0; + transition: + color 0.2s, + border-color 0.2s, + transform 0.25s ease; +} + +.expand-btn:hover { + color: #4dd0e1; + border-color: #4dd0e1; +} + +.expand-btn .chevron-icon { + width: 14px; + height: 14px; + transition: transform 0.25s ease; +} + +.expand-btn.expanded .chevron-icon { + transform: rotate(180deg); +} + /* ─── Content Preview ────────────────────────────────── */ .log-preview { @@ -79,10 +114,17 @@ word-break: break-all; margin: 0; opacity: 0.8; + transition: + max-height 0.35s ease, + opacity 0.2s; } .log-entry.selected .log-preview { - max-height: 120px; + opacity: 1; +} + +.log-preview--expanded { + max-height: 2000px; opacity: 1; } diff --git a/client/src/components/LogList.jsx b/client/src/components/LogList.jsx index 5d6e8fe..e443f76 100644 --- a/client/src/components/LogList.jsx +++ b/client/src/components/LogList.jsx @@ -1,4 +1,6 @@ +import { useState } from "react"; import "./LogList.css"; +import { AnalyzeIcon, ChevronIcon } from "@/components/icons"; const SEVERITY_COLORS = { INFO: "#4dd0e1", @@ -14,15 +16,31 @@ export default function LogList({ onAnalyze, analyzing, }) { + const [expandedIds, setExpandedIds] = useState(new Set()); + + const toggleExpand = (id, e) => { + e.stopPropagation(); + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + return (
{logs.map((log) => { const isSelected = selectedId === log.id; + const isExpanded = expandedIds.has(log.id); return (
onSelect(log.id)} >
@@ -37,9 +55,20 @@ export default function LogList({ {log.serviceName} {log.type.replace(/_/g, " ")} +
-
{log.logContent}
+
+							{log.logContent}
+						
@@ -62,26 +91,7 @@ export default function LogList({ ) : ( <> - + Analyze )} From 79c99023c90382fa0bdfa110efb9726818030dab Mon Sep 17 00:00:00 2001 From: Sehmuel Wagner <111253615+sachmii@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:03:58 +0200 Subject: [PATCH 3/7] fix docker error, wrong name for rabbitmq host --- infra/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 8310413..91132fb 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -58,7 +58,7 @@ services: - SPRING_DATASOURCE_URL=${POSTGRES_URL:-jdbc:postgresql://postgres:5432/appdb} - SPRING_DATASOURCE_USERNAME=${POSTGRES_USER:-user} - SPRING_DATASOURCE_PASSWORD=${POSTGRES_PASSWORD:-password} - - SPRING_RABBITMQ_HOST=${RABBITMQ_HOST:-localhost} + - SPRING_RABBITMQ_HOST=${RABBITMQ_HOST:-rabbitmq} - SPRING_RABBITMQ_PORT=${RABBITMQ_PORT:-5672} - SPRING_RABBITMQ_USERNAME=${RABBITMQ_USER:-guest} - SPRING_RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD:-guest} From 6a4cb2129dbc070f72804c9150f6f97611279a57 Mon Sep 17 00:00:00 2001 From: Sehmuel Wagner <111253615+sachmii@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:08:56 +0200 Subject: [PATCH 4/7] add client tests --- client/src/test/InsightsPanel.test.jsx | 97 ++++++++++++++++++ client/src/test/LogList.test.jsx | 134 +++++++++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 client/src/test/InsightsPanel.test.jsx create mode 100644 client/src/test/LogList.test.jsx diff --git a/client/src/test/InsightsPanel.test.jsx b/client/src/test/InsightsPanel.test.jsx new file mode 100644 index 0000000..ee1e47e --- /dev/null +++ b/client/src/test/InsightsPanel.test.jsx @@ -0,0 +1,97 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import InsightsPanel from "../components/InsightsPanel"; + +const mockResult = { + confidence: "high", + problem_type: "Database Connection Error", + severity: "CRITICAL", + summary: "The application failed to connect to the PostgreSQL database on startup.", + problem_summary: "PostgreSQL database port 5432 is not accessible, probably because the container is not running or network partition exists.", + evidence: [ + "Connection refused to localhost:5432", + "HikariPool-1 - Connection not available" + ], + troubleshoot: [ + "Check if PostgreSQL container is running", + "Verify the connection URL in application.properties" + ], + solutions: [ + "Start the database container using docker compose up postgres", + "Correct the datasource credentials" + ], + sources: [ + { title: "Spring Boot DB configuration guide", snippet: "Ensure spring.datasource.url is pointing to the correct host" }, + { id: "StackOverflow #12345", title: "Connection Refused guide" } + ] +}; + +describe("InsightsPanel", () => { + it("should render loading state when loading is true", () => { + render(React.createElement(InsightsPanel, { loading: true, result: null })); + expect(screen.getByText("Analyzing…")).toBeInTheDocument(); + expect(screen.getByText("Running AI analysis on selected log")).toBeInTheDocument(); + }); + + it("should render null when result is not provided and not loading", () => { + const { container } = render(React.createElement(InsightsPanel, { loading: false, result: null })); + expect(container.firstChild).toBeNull(); + }); + + it("should render all panel sections when result is provided", () => { + render(React.createElement(InsightsPanel, { loading: false, result: mockResult })); + + // Header checks + expect(screen.getByText("Database Connection Error")).toBeInTheDocument(); + expect(screen.getByText("CRITICAL")).toBeInTheDocument(); + expect(screen.getByText("high confidence")).toBeInTheDocument(); + + // Sections checks + expect(screen.getByText("Summary")).toBeInTheDocument(); + expect(screen.getByText(mockResult.summary)).toBeInTheDocument(); + + expect(screen.getByText("Root Cause")).toBeInTheDocument(); + expect(screen.getByText(mockResult.problem_summary)).toBeInTheDocument(); + + expect(screen.getByText("Evidence")).toBeInTheDocument(); + expect(screen.getByText("Connection refused to localhost:5432")).toBeInTheDocument(); + expect(screen.getByText("HikariPool-1 - Connection not available")).toBeInTheDocument(); + + expect(screen.getByText("Troubleshooting Steps")).toBeInTheDocument(); + expect(screen.getByText("Check if PostgreSQL container is running")).toBeInTheDocument(); + + expect(screen.getByText("Proposed Solutions")).toBeInTheDocument(); + expect(screen.getByText("Start the database container using docker compose up postgres")).toBeInTheDocument(); + + expect(screen.getByText("Sources")).toBeInTheDocument(); + expect(screen.getByText("Spring Boot DB configuration guide")).toBeInTheDocument(); + expect(screen.getByText("Ensure spring.datasource.url is pointing to the correct host")).toBeInTheDocument(); + expect(screen.getByText("Connection Refused guide")).toBeInTheDocument(); + }); + + it("should only render available sections if some fields are missing in the result", () => { + const minimalResult = { + confidence: "medium", + problem_type: "Network Timeout", + severity: "WARNING", + summary: "Brief timeout error" + }; + + render(React.createElement(InsightsPanel, { loading: false, result: minimalResult })); + + expect(screen.getByText("Network Timeout")).toBeInTheDocument(); + expect(screen.getByText("WARNING")).toBeInTheDocument(); + expect(screen.getByText("medium confidence")).toBeInTheDocument(); + + expect(screen.getByText("Summary")).toBeInTheDocument(); + expect(screen.getByText("Brief timeout error")).toBeInTheDocument(); + + // These sections should not exist + expect(screen.queryByText("Root Cause")).not.toBeInTheDocument(); + expect(screen.queryByText("Evidence")).not.toBeInTheDocument(); + expect(screen.queryByText("Troubleshooting Steps")).not.toBeInTheDocument(); + expect(screen.queryByText("Proposed Solutions")).not.toBeInTheDocument(); + expect(screen.queryByText("Sources")).not.toBeInTheDocument(); + }); +}); diff --git a/client/src/test/LogList.test.jsx b/client/src/test/LogList.test.jsx new file mode 100644 index 0000000..8089472 --- /dev/null +++ b/client/src/test/LogList.test.jsx @@ -0,0 +1,134 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import LogList from "../components/LogList"; + +const mockLogs = [ + { + id: 1, + serviceName: "auth-service", + logContent: "User login failed due to invalid credentials", + severity: "WARNING", + type: "TROUBLESHOOTING_NOTE", + timestamp: "2026-06-16T12:00:00Z", + }, + { + id: 2, + serviceName: "api-gateway", + logContent: "NullPointerException in routing controller", + severity: "ERROR", + type: "BUILD_ERRORS", + timestamp: "2026-06-16T12:01:00Z", + }, +]; + +function renderLogList(props = {}) { + const defaultProps = { + logs: mockLogs, + selectedId: null, + onSelect: vi.fn(), + onAnalyze: vi.fn(), + analyzing: false, + ...props, + }; + return { + ...render(React.createElement(LogList, defaultProps)), + ...defaultProps, + }; +} + +describe("LogList", () => { + it("should render all log entries with service details", () => { + renderLogList(); + + expect(screen.getByText("auth-service")).toBeInTheDocument(); + expect(screen.getByText("api-gateway")).toBeInTheDocument(); + + expect(screen.getByText("WARNING")).toBeInTheDocument(); + expect(screen.getByText("ERROR")).toBeInTheDocument(); + + expect(screen.getByText("TROUBLESHOOTING NOTE")).toBeInTheDocument(); + expect(screen.getByText("BUILD ERRORS")).toBeInTheDocument(); + + expect(screen.getByText("User login failed due to invalid credentials")).toBeInTheDocument(); + expect(screen.getByText("NullPointerException in routing controller")).toBeInTheDocument(); + }); + + it("should call onSelect when clicking on a log entry", () => { + const { onSelect } = renderLogList(); + + const entry = screen.getByText("auth-service").closest(".log-entry"); + fireEvent.click(entry); + + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onSelect).toHaveBeenCalledWith(1); + }); + + it("should show selected styling and the Analyze button only for the selected log", () => { + const { container } = renderLogList({ selectedId: 2 }); + + const entries = container.querySelectorAll(".log-entry"); + expect(entries[0]).not.toHaveClass("selected"); + expect(entries[1]).toHaveClass("selected"); + + // Analyze button should only be in the second log + const analyzeBtns = screen.getAllByRole("button", { name: /analyze/i }); + expect(analyzeBtns).toHaveLength(1); + expect(entries[1]).toContainElement(analyzeBtns[0]); + }); + + it("should trigger onAnalyze when Analyze button is clicked", () => { + const { onAnalyze } = renderLogList({ selectedId: 2 }); + + const analyzeBtn = screen.getByRole("button", { name: /analyze/i }); + fireEvent.click(analyzeBtn); + + expect(onAnalyze).toHaveBeenCalledTimes(1); + expect(onAnalyze).toHaveBeenCalledWith(mockLogs[1]); + }); + + it("should disable Analyze button and show analyzing text when analyzing is true", () => { + renderLogList({ selectedId: 2, analyzing: true }); + + const analyzingBtn = screen.getByRole("button", { name: /analyzing/i }); + expect(analyzingBtn).toBeDisabled(); + expect(analyzingBtn).toHaveTextContent("Analyzing…"); + }); + + it("should toggle expanded class and log-preview--expanded class when clicking expand button", () => { + const { container } = renderLogList(); + + const firstEntry = container.querySelectorAll(".log-entry")[0]; + const firstPreview = firstEntry.querySelector(".log-preview"); + const expandBtn = firstEntry.querySelector(".expand-btn"); + + expect(firstEntry).not.toHaveClass("expanded"); + expect(firstPreview).not.toHaveClass("log-preview--expanded"); + expect(expandBtn).toHaveAttribute("aria-expanded", "false"); + + // Click expand + fireEvent.click(expandBtn); + + expect(firstEntry).toHaveClass("expanded"); + expect(firstPreview).toHaveClass("log-preview--expanded"); + expect(expandBtn).toHaveAttribute("aria-expanded", "true"); + + // Click collapse + fireEvent.click(expandBtn); + + expect(firstEntry).not.toHaveClass("expanded"); + expect(firstPreview).not.toHaveClass("log-preview--expanded"); + expect(expandBtn).toHaveAttribute("aria-expanded", "false"); + }); + + it("should prevent event propagation when clicking expand button so onSelect is not called", () => { + const { onSelect, container } = renderLogList(); + + const firstEntry = container.querySelectorAll(".log-entry")[0]; + const expandBtn = firstEntry.querySelector(".expand-btn"); + + fireEvent.click(expandBtn); + + expect(onSelect).not.toHaveBeenCalled(); + }); +}); From 9c43d85dc0defabcfea1448be4a914f659ef942b Mon Sep 17 00:00:00 2001 From: Sehmuel Wagner <111253615+sachmii@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:24:32 +0200 Subject: [PATCH 5/7] add changes --- client/src/App.css | 41 +++++++ client/src/App.jsx | 71 ++++++++++- client/src/components/InsightsPanel.css | 48 ++++++++ client/src/components/InsightsPanel.jsx | 60 +++++++++- client/src/components/ResolveModal.css | 101 ++++++++++++++++ client/src/components/ResolveModal.jsx | 150 ++++++++++++++++++++++++ client/src/lib/api.js | 29 +++++ client/src/test/InsightsPanel.test.jsx | 44 ++++++- client/src/test/ResolveModal.test.jsx | 110 +++++++++++++++++ 9 files changed, 649 insertions(+), 5 deletions(-) create mode 100644 client/src/components/ResolveModal.css create mode 100644 client/src/components/ResolveModal.jsx create mode 100644 client/src/test/ResolveModal.test.jsx diff --git a/client/src/App.css b/client/src/App.css index 0c29a7c..3984be0 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -296,3 +296,44 @@ align-items: flex-start; } } + +/* ─── Success Toast ──────────────────────────────────── */ + +.toast { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 200; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + border: 1px solid rgba(46, 213, 115, 0.4); + background: rgba(13, 19, 47, 0.95); + color: #2ed573; + font-size: 12px; + letter-spacing: 0.1em; + box-shadow: + 0 0 0 1px rgba(46, 213, 115, 0.15), + 0 8px 32px rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); + animation: toastIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.toast-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +@keyframes toastIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + diff --git a/client/src/App.jsx b/client/src/App.jsx index 2865021..f93446f 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -2,11 +2,12 @@ import { useState, useEffect, useCallback } from "react"; import "./App.css"; import PrivacyToggle from "@/components/PrivacyToggle"; import IngestModal from "@/components/IngestModal"; +import ResolveModal from "@/components/ResolveModal"; import LogList from "@/components/LogList"; import InsightsPanel from "@/components/InsightsPanel"; import { IngestIcon, EmptyLogsIcon, EmptyInsightsIcon } from "@/components/icons"; import { usePrivacyMode } from "@/context/PrivacyModeContext"; -import { ingestLog, analyzeLog } from "@/lib/api"; +import { ingestLog, analyzeLog, submitRagDocument } from "@/lib/api"; let nextLogId = 1; @@ -22,6 +23,10 @@ function App() { const [analyzing, setAnalyzing] = useState(false); const [analysisError, setAnalysisError] = useState(null); + const [showResolveModal, setShowResolveModal] = useState(false); + const [resolved, setResolved] = useState(false); + const [notification, setNotification] = useState(null); + const [clock, setClock] = useState(new Date()); /* ── Live clock ─────────────────────────────────────── */ @@ -30,6 +35,13 @@ function App() { return () => clearInterval(timer); }, []); + /* ── Auto-dismiss notification ──────────────────────── */ + useEffect(() => { + if (!notification) return; + const timer = setTimeout(() => setNotification(null), 3000); + return () => clearTimeout(timer); + }, [notification]); + /* ── Handlers ───────────────────────────────────────── */ const handleIngest = useCallback(async (payload) => { await ingestLog(payload); @@ -41,6 +53,7 @@ function App() { setAnalyzing(true); setAnalysisError(null); setAnalysisResult(null); + setResolved(false); try { const result = await analyzeLog(log.logContent, mode); @@ -58,6 +71,25 @@ function App() { setSelectedLogId((prev) => (prev === id ? null : id)); }, []); + const handleResolve = useCallback( + async (type, solutionText) => { + if (type === "rag" && solutionText) { + const title = analysisResult?.problem_type || "Resolved Issue"; + await submitRagDocument(title, solutionText, [ + analysisResult?.severity || "unknown", + "user-solution", + ]); + } + setResolved(true); + setNotification( + type === "rag" + ? "Issue resolved — solution submitted to knowledge base" + : "Issue marked as resolved", + ); + }, + [analysisResult], + ); + /* ── Render ─────────────────────────────────────────── */ const hasLogs = logs.length > 0; @@ -125,7 +157,12 @@ function App() {
) : analysisResult ? (
- + setShowResolveModal(true)} + />
) : (
@@ -172,6 +209,36 @@ function App() { onClose={() => setShowIngestModal(false)} /> )} + + {/* ── Resolve Modal ─────────────────────────────── */} + {showResolveModal && ( + setShowResolveModal(false)} + /> + )} + + {/* ── Success Notification Toast ────────────────── */} + {notification && ( +
+ + {notification} +
+ )}
); } diff --git a/client/src/components/InsightsPanel.css b/client/src/components/InsightsPanel.css index 467fe2a..d81098c 100644 --- a/client/src/components/InsightsPanel.css +++ b/client/src/components/InsightsPanel.css @@ -168,3 +168,51 @@ letter-spacing: 0.12em; margin: 0; } + +/* ─── Resolution Action ─────────────────────────────── */ + +.insights-resolve-section { + margin-top: 4px; + padding-top: 14px; +} + +.resolve-btn { + border-color: #4dd0e1 !important; + color: #b8f2ff !important; + background: rgba(77, 208, 225, 0.06) !important; + padding: 10px 20px !important; + font-size: 12px !important; + letter-spacing: 0.14em; + transition: + background 0.2s, + box-shadow 0.2s, + border-color 0.2s; +} + +.resolve-btn:hover { + background: rgba(77, 208, 225, 0.15) !important; + box-shadow: + 0 0 16px rgba(77, 208, 225, 0.25), + 0 0 0 1px rgba(77, 208, 225, 0.3); +} + +.resolved-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border: 1px solid rgba(46, 213, 115, 0.4); + background: rgba(46, 213, 115, 0.08); + color: #2ed573; + font-size: 12px; + letter-spacing: 0.16em; + text-transform: uppercase; + font-weight: 600; + animation: fadeIn 0.3s ease; +} + +.resolved-icon { + width: 16px; + height: 16px; +} + diff --git a/client/src/components/InsightsPanel.jsx b/client/src/components/InsightsPanel.jsx index 22b64c5..4a89757 100644 --- a/client/src/components/InsightsPanel.jsx +++ b/client/src/components/InsightsPanel.jsx @@ -6,7 +6,12 @@ const CONFIDENCE_COLORS = { high: "#4dd0e1", }; -export default function InsightsPanel({ result, loading }) { +export default function InsightsPanel({ + result, + loading, + resolved, + onMarkResolved, +}) { if (loading) { return (
@@ -117,6 +122,59 @@ export default function InsightsPanel({ result, loading }) {
)} + + {/* ── Resolution Action ────────────────────────── */} +
+ {resolved ? ( +
+ + Resolved +
+ ) : ( + + )} +
); } diff --git a/client/src/components/ResolveModal.css b/client/src/components/ResolveModal.css new file mode 100644 index 0000000..5764b92 --- /dev/null +++ b/client/src/components/ResolveModal.css @@ -0,0 +1,101 @@ +/* ─── Resolve Modal ──────────────────────────────────── */ + +.resolve-modal { + max-width: 520px; +} + +.resolve-body { + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ─── Issue Context ──────────────────────────────────── */ + +.resolve-context { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px solid #1e2d5f; + background: rgba(10, 14, 39, 0.6); +} + +.resolve-context-label { + font-size: 10px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: #5a7aa3; +} + +.resolve-context-value { + font-size: 11px; + color: #b8f2ff; + letter-spacing: 0.06em; +} + +/* ─── Sections ───────────────────────────────────────── */ + +.resolve-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.resolve-section-label { + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: #4dd0e1; + text-shadow: 0 0 6px rgba(77, 208, 225, 0.3); +} + +.resolve-section-desc { + font-size: 11px; + color: #6b8fb5; + line-height: 1.5; + margin: 0; +} + +/* ─── Simple Resolve Button ──────────────────────────── */ + +.resolve-simple-btn { + align-self: flex-start; + border-color: #36526e !important; +} + +.resolve-simple-btn:hover:not(:disabled) { + border-color: #4dd0e1 !important; + color: #b8f2ff !important; + box-shadow: 0 0 10px rgba(77, 208, 225, 0.15); +} + +/* ─── RAG Submit Button ──────────────────────────────── */ + +.resolve-rag-btn { + align-self: flex-start; +} + +/* ─── Divider ────────────────────────────────────────── */ + +.resolve-divider { + display: flex; + align-items: center; + gap: 12px; +} + +.resolve-divider::before, +.resolve-divider::after { + content: ""; + flex: 1; + height: 1px; + background: #1e2d5f; +} + +.resolve-divider-text { + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: #3b5d7c; +} diff --git a/client/src/components/ResolveModal.jsx b/client/src/components/ResolveModal.jsx new file mode 100644 index 0000000..9ee6d2b --- /dev/null +++ b/client/src/components/ResolveModal.jsx @@ -0,0 +1,150 @@ +import { useState } from "react"; +import "./ResolveModal.css"; + +export default function ResolveModal({ result, onResolve, onClose }) { + const [solutionText, setSolutionText] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const canSubmitRag = solutionText.trim().length > 0; + + async function handleSimpleClose() { + setSubmitting(true); + setError(null); + try { + await onResolve("simple"); + onClose(); + } catch (err) { + setError(err.message || "Failed to resolve issue"); + } finally { + setSubmitting(false); + } + } + + async function handleRagSubmit(e) { + e.preventDefault(); + if (!canSubmitRag || submitting) return; + + setSubmitting(true); + setError(null); + try { + await onResolve("rag", solutionText.trim()); + onClose(); + } catch (err) { + setError(err.message || "Failed to submit solution"); + } finally { + setSubmitting(false); + } + } + + return ( +
+
e.stopPropagation()} + > +
+
Resolve Issue
+ +
+ +
+ {/* Issue context */} + {result && ( +
+ Issue: + + {result.problem_type} + {result.severity && ` — ${result.severity}`} + +
+ )} + + {/* ── Option A: Simple Close ────────────────── */} +
+
Option A — Quick Resolve
+

+ Mark this issue as resolved without additional details. +

+ +
+ + {/* ── Divider ───────────────────────────────── */} +
+ or +
+ + {/* ── Option B: RAG Feedback ────────────────── */} +
+
+ Option B — Submit to Knowledge Base +
+

+ Share your custom solution or root-cause fix to improve future AI + analyses via the RAG system. +

+
+ +