diff --git a/client/src/App.css b/client/src/App.css
index 0c29a7c..b378328 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -1,18 +1,18 @@
.page {
min-height: 100vh;
- background: #0a0e27;
- color: #8bb4d9;
+ background: var(--background);
+ color: var(--foreground);
position: relative;
- padding: 10px;
+ padding: 20px;
}
.frame {
- max-width: 1320px;
+ max-width: 1500px;
margin: 0 auto;
- border: 2px solid #2a3a6a;
- background: #0d132f;
- box-shadow: 0 0 0 2px rgba(77, 208, 225, 0.25);
- padding: 16px;
+ border: 2px solid var(--border);
+ background: var(--background);
+ box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.25);
+ padding: 24px;
position: relative;
}
@@ -21,10 +21,10 @@
inset: 0;
background: repeating-linear-gradient(
0deg,
- rgba(10, 14, 39, 0),
- rgba(10, 14, 39, 0) 2px,
- rgba(77, 208, 225, 0.05) 2px,
- rgba(77, 208, 225, 0.05) 4px
+ transparent,
+ transparent 2px,
+ rgba(var(--primary-rgb), 0.05) 2px,
+ rgba(var(--primary-rgb), 0.05) 4px
);
opacity: 0.4;
pointer-events: none;
@@ -34,136 +34,160 @@
display: flex;
align-items: center;
justify-content: space-between;
- border: 2px solid #2a3a6a;
- background: #12183a;
- padding: 14px 16px;
- gap: 16px;
+ border: 2px solid var(--border);
+ background: var(--card);
+ padding: 18px 24px;
+ gap: 20px;
}
.brand {
display: flex;
align-items: center;
- gap: 14px;
+ gap: 18px;
}
.brand-mark {
- color: #4dd0e1;
- font-size: 22px;
+ color: var(--primary);
+ font-size: 28px;
letter-spacing: 0.15em;
+ animation: cursorBlink 1.4s step-end infinite;
+}
+
+@keyframes cursorBlink {
+ from, to { opacity: 0; }
+ 50% { opacity: 1; }
}
.brand-title {
margin: 0;
- font-size: 22px;
- color: #9be6f6;
- letter-spacing: 0.08em;
+ font-size: 28px;
+ color: var(--foreground);
+ letter-spacing: 0.15em;
+ text-transform: uppercase;
text-shadow:
- 0 0 6px rgba(77, 208, 225, 0.6),
- 0 0 14px rgba(77, 208, 225, 0.45),
- 0 0 24px rgba(77, 208, 225, 0.25);
+ 0 0 8px rgba(var(--primary-rgb), 0.65),
+ 0 0 16px rgba(var(--primary-rgb), 0.45);
+ animation: textGlowPulse 4s ease-in-out infinite alternate;
+}
+
+@keyframes textGlowPulse {
+ 0% {
+ text-shadow:
+ 0 0 6px rgba(var(--primary-rgb), 0.5),
+ 0 0 12px rgba(var(--primary-rgb), 0.35);
+ opacity: 0.93;
+ }
+ 100% {
+ text-shadow:
+ 0 0 14px rgba(var(--primary-rgb), 0.8),
+ 0 0 26px rgba(var(--primary-rgb), 0.55);
+ opacity: 1;
+ }
}
.brand-sub {
margin: 4px 0 0;
- font-size: 12px;
+ font-size: 14px;
letter-spacing: 0.18em;
text-transform: uppercase;
- color: #5a7aa3;
+ color: var(--muted-foreground);
}
.actions {
display: flex;
align-items: center;
- gap: 10px;
+ gap: 12px;
}
.ghost-btn {
- border: 1px solid #36526e;
+ border: 1px solid var(--border);
background: transparent;
- color: #8bb4d9;
- padding: 8px 14px;
- font-size: 12px;
+ color: var(--foreground);
+ padding: 10px 18px;
+ font-size: 14px;
letter-spacing: 0.18em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
- gap: 8px;
+ gap: 10px;
cursor: pointer;
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.ghost-btn:hover {
- border-color: #4dd0e1;
- color: #b8f2ff;
+ border-color: var(--primary);
+ color: var(--primary);
+ box-shadow: 0 0 12px rgba(var(--primary-rgb), 0.35);
+ text-shadow: 0 0 6px rgba(var(--primary-rgb), 0.5);
}
.ghost-btn.compact {
- padding: 6px 12px;
- font-size: 11px;
+ padding: 8px 14px;
+ font-size: 13px;
}
.ghost-icon {
- color: #4dd0e1;
- width: 16px;
- height: 16px;
+ color: var(--primary);
+ width: 18px;
+ height: 18px;
}
.cloud-btn {
- border-color: #3a5573;
- box-shadow: inset 0 0 0 1px rgba(77, 208, 225, 0.35);
- background: rgba(12, 18, 40, 0.6);
- padding: 8px 18px;
+ border-color: var(--border);
+ box-shadow: inset 0 0 0 1px rgba(var(--primary-rgb), 0.35);
+ background: var(--background);
+ padding: 10px 22px;
}
.cloud-icon {
- width: 15px;
- height: 15px;
+ width: 17px;
+ height: 17px;
}
.pulse-btn {
- border: 1px solid #36526e;
+ border: 1px solid var(--border);
background: transparent;
- color: #4dd0e1;
- width: 34px;
- height: 34px;
- font-size: 16px;
+ color: var(--primary);
+ width: 42px;
+ height: 42px;
+ font-size: 20px;
cursor: pointer;
}
.pulse-icon {
- width: 18px;
- height: 18px;
+ width: 22px;
+ height: 22px;
}
.content {
- margin-top: 16px;
+ margin-top: 24px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 16px;
+ gap: 24px;
}
.panel {
- border: 2px solid #2a3a6a;
- background: #131a3a;
- min-height: 480px;
+ border: 2px solid var(--border);
+ background: var(--card);
+ min-height: 580px;
position: relative;
}
.panel-tag {
position: absolute;
- top: -12px;
- left: 14px;
- background: #0d132f;
- padding: 2px 10px;
- border: 1px solid #2a3a6a;
- font-size: 11px;
+ top: -14px;
+ left: 18px;
+ background: var(--background);
+ border: 1px solid var(--border);
+ font-size: 13px;
letter-spacing: 0.2em;
text-transform: uppercase;
- color: #7bb3c8;
+ color: var(--primary);
}
.panel-body {
height: 100%;
- padding: 32px;
+ padding: 40px;
}
.panel-body.empty {
@@ -172,36 +196,36 @@
align-items: center;
justify-content: center;
text-align: center;
- gap: 16px;
+ gap: 20px;
}
.empty-icon {
- width: 64px;
- height: 64px;
- color: #3b5d7c;
+ width: 80px;
+ height: 80px;
+ color: var(--muted-foreground);
}
.empty-title {
- font-size: 12px;
+ font-size: 14px;
letter-spacing: 0.2em;
text-transform: uppercase;
- color: #6b8fb5;
+ color: var(--muted-foreground);
line-height: 1.6;
}
.accent {
- color: #4dd0e1;
+ color: var(--primary);
}
.empty-dots {
display: inline-flex;
- gap: 6px;
+ gap: 8px;
}
.empty-dots .dot {
- width: 6px;
- height: 6px;
- background: #2a3a6a;
+ width: 8px;
+ height: 8px;
+ background: var(--border);
display: inline-block;
animation: dotPulse 1.4s ease-in-out infinite;
}
@@ -223,24 +247,24 @@
100% {
opacity: 0.35;
transform: translateY(0);
- background: #2a3a6a;
+ background: var(--border);
}
50% {
opacity: 1;
transform: translateY(-3px);
- background: #4dd0e1;
+ background: var(--primary);
}
}
.statusbar {
- margin-top: 16px;
- border: 2px solid #2a3a6a;
- background: #12183a;
- padding: 8px 12px;
+ margin-top: 24px;
+ border: 2px solid var(--border);
+ background: var(--card);
+ padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
- font-size: 11px;
+ font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
@@ -248,34 +272,51 @@
.status-left {
display: inline-flex;
align-items: center;
- gap: 10px;
+ gap: 12px;
}
.status-dot {
- width: 8px;
- height: 8px;
- background: #4dd0e1;
- box-shadow: 0 0 10px rgba(77, 208, 225, 0.8);
+ width: 10px;
+ height: 10px;
+ background: var(--primary);
+ box-shadow: 0 0 10px rgba(var(--primary-rgb), 0.8);
border-radius: 2px;
}
.divider {
width: 1px;
- height: 12px;
- background: #2a3a6a;
+ height: 14px;
+ background: var(--border);
display: inline-block;
}
.status-right {
- color: #5a7aa3;
+ color: var(--muted-foreground);
}
.mode-indicator {
- color: #4dd0e1;
+ color: var(--primary);
+}
+
+.theme-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--primary);
+ padding: 6px 12px;
+ font-size: 13px;
+ cursor: pointer;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ transition: all 0.2s ease;
+}
+
+.theme-btn:hover {
+ border-color: var(--primary);
+ box-shadow: 0 0 8px rgba(var(--primary-rgb), 0.3);
}
.error-text {
- color: #ff6b9d !important;
+ color: var(--destructive) !important;
}
.panel-body:not(.empty) {
@@ -288,7 +329,7 @@
}
.panel {
- min-height: 360px;
+ min-height: 420px;
}
.topbar {
@@ -296,3 +337,43 @@
align-items: flex-start;
}
}
+
+/* ─── Success Toast ──────────────────────────────────── */
+
+.toast {
+ position: fixed;
+ bottom: 32px;
+ right: 32px;
+ z-index: 200;
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 26px;
+ border: 1px solid var(--primary);
+ background: var(--card);
+ color: var(--primary);
+ font-size: 14px;
+ letter-spacing: 0.1em;
+ box-shadow:
+ 0 0 0 1px rgba(var(--primary-rgb), 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: 18px;
+ height: 18px;
+ 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 facd4dd..37bf4fc 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -2,10 +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;
@@ -13,22 +15,51 @@ function App() {
/* ── State ──────────────────────────────────────────── */
const { mode } = usePrivacyMode();
+ const [theme, setTheme] = useState(() => {
+ try {
+ if (typeof localStorage !== "undefined" && typeof localStorage.getItem === "function") {
+ return localStorage.getItem("devpulse-theme") || "cyan";
+ }
+ } catch {
+ // ignore
+ }
+ return "cyan";
+ });
const [logs, setLogs] = useState([]);
const [selectedLogId, setSelectedLogId] = useState(null);
const [showIngestModal, setShowIngestModal] = useState(false);
- const [analysisResult, setAnalysisResult] = useState(null);
const [analyzing, setAnalyzing] = useState(false);
const [analysisError, setAnalysisError] = useState(null);
+ const [showResolveModal, setShowResolveModal] = useState(false);
+ const [notification, setNotification] = useState(null);
+
const [clock, setClock] = useState(new Date());
+ useEffect(() => {
+ try {
+ if (typeof localStorage !== "undefined" && typeof localStorage.setItem === "function") {
+ localStorage.setItem("devpulse-theme", theme);
+ }
+ } catch {
+ // ignore
+ }
+ }, [theme]);
+
/* ── Live clock ─────────────────────────────────────── */
useEffect(() => {
const timer = setInterval(() => setClock(new Date()), 1000);
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);
@@ -39,11 +70,14 @@ function App() {
async (log) => {
setAnalyzing(true);
setAnalysisError(null);
- setAnalysisResult(null);
try {
const result = await analyzeLog(log.logContent, mode);
- setAnalysisResult(result);
+ setLogs((prev) =>
+ prev.map((l) =>
+ l.id === log.id ? { ...l, analysis: result, resolved: false } : l
+ )
+ );
} catch (err) {
setAnalysisError(err.message || "Analysis failed");
} finally {
@@ -57,19 +91,53 @@ function App() {
setSelectedLogId((prev) => (prev === id ? null : id));
}, []);
+ const handleDelete = useCallback((id) => {
+ setLogs((prev) => prev.filter((l) => l.id !== id));
+ setSelectedLogId((prev) => (prev === id ? null : prev));
+ setNotification("Log entry removed");
+ }, []);
+
+ const handleResolve = useCallback(
+ async (type, solutionText) => {
+ const selectedLog = logs.find((l) => l.id === selectedLogId);
+ const activeAnalysis = selectedLog?.analysis;
+ if (type === "rag" && solutionText) {
+ const title = activeAnalysis?.problem_type || "Resolved Issue";
+ await submitRagDocument(title, solutionText, [
+ activeAnalysis?.severity || "unknown",
+ "user-solution",
+ ]);
+ }
+ setLogs((prev) =>
+ prev.map((l) =>
+ l.id === selectedLogId ? { ...l, resolved: true } : l
+ )
+ );
+ setNotification(
+ type === "rag"
+ ? "Issue resolved — solution submitted to knowledge base"
+ : "Issue marked as resolved",
+ );
+ },
+ [logs, selectedLogId],
+ );
+
/* ── Render ─────────────────────────────────────────── */
const hasLogs = logs.length > 0;
+ const selectedLog = logs.find((l) => l.id === selectedLogId);
+ const currentAnalysisResult = selectedLog?.analysis || null;
+ const isCurrentLogResolved = selectedLog?.resolved || false;
return (
-
+
@@ -125,34 +180,12 @@ function App() {
onSelect={handleSelectLog}
onAnalyze={handleAnalyze}
analyzing={analyzing}
+ onDelete={handleDelete}
/>
) : (
-
+
No Logs Ingested
- ) : analysisResult ? (
+ ) : currentAnalysisResult ? (
-
+ setShowResolveModal(true)}
+ />
) : (
-
+
{analysisError ? (
{analysisError}
) : (
@@ -232,6 +247,15 @@ function App() {
Mode: {mode === "local" ? "🔒 Local" : "☁️ Cloud"}
+
+
{clock.toLocaleString()}
@@ -244,6 +268,36 @@ function App() {
onClose={() => setShowIngestModal(false)}
/>
)}
+
+ {/* ── Resolve Modal ─────────────────────────────── */}
+ {showResolveModal && (
+
setShowResolveModal(false)}
+ />
+ )}
+
+ {/* ── Success Notification Toast ────────────────── */}
+ {notification && (
+
+ )}
);
}
diff --git a/client/src/components/IngestModal.css b/client/src/components/IngestModal.css
index d786b30..7d2b50a 100644
--- a/client/src/components/IngestModal.css
+++ b/client/src/components/IngestModal.css
@@ -9,7 +9,7 @@
display: flex;
align-items: center;
justify-content: center;
- padding: 16px;
+ padding: 24px;
animation: fadeIn 0.2s ease;
}
@@ -22,11 +22,11 @@
.modal {
width: 100%;
- max-width: 560px;
- border: 2px solid #2a3a6a;
- background: #0d132f;
+ max-width: 640px;
+ border: 2px solid var(--border);
+ background: var(--background);
box-shadow:
- 0 0 0 2px rgba(77, 208, 225, 0.2),
+ 0 0 0 2px rgba(var(--primary-rgb), 0.2),
0 24px 80px rgba(0, 0, 0, 0.6);
animation: slideUp 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -46,62 +46,62 @@
display: flex;
align-items: center;
justify-content: space-between;
- border-bottom: 1px solid #2a3a6a;
- padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ padding: 16px 22px;
}
.modal-tag {
- font-size: 12px;
+ font-size: 14px;
letter-spacing: 0.2em;
text-transform: uppercase;
- color: #4dd0e1;
- text-shadow: 0 0 8px rgba(77, 208, 225, 0.4);
+ color: var(--primary);
+ text-shadow: 0 0 8px rgba(var(--primary-rgb), 0.4);
}
.modal-close {
border: none;
background: transparent;
- color: #5a7aa3;
- font-size: 16px;
+ color: var(--muted-foreground);
+ font-size: 18px;
cursor: pointer;
- padding: 4px 8px;
+ padding: 6px 10px;
transition: color 0.2s;
}
.modal-close:hover {
- color: #ff6b9d;
+ color: var(--destructive);
}
/* ─── Form ───────────────────────────────────────────── */
.modal-form {
- padding: 16px;
+ padding: 24px;
display: flex;
flex-direction: column;
- gap: 14px;
+ gap: 18px;
}
.form-group {
display: flex;
flex-direction: column;
- gap: 4px;
+ gap: 6px;
}
.form-label {
- font-size: 10px;
+ font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
- color: #5a7aa3;
+ color: var(--muted-foreground);
}
.form-input,
.form-textarea,
.form-select {
- border: 1px solid #2a3a6a;
- background: rgba(10, 14, 39, 0.8);
- color: #8bb4d9;
- padding: 8px 10px;
- font-size: 12px;
+ border: 1px solid var(--border);
+ background: var(--background);
+ color: var(--foreground);
+ padding: 10px 14px;
+ font-size: 14px;
font-family: inherit;
letter-spacing: 0.04em;
transition: border-color 0.2s;
@@ -111,18 +111,18 @@
.form-textarea:focus,
.form-select:focus {
outline: none;
- border-color: #4dd0e1;
- box-shadow: 0 0 0 1px rgba(77, 208, 225, 0.25);
+ border-color: var(--primary);
+ box-shadow: 0 0 0 1px rgba(var(--primary-rgb), 0.25);
}
.form-input::placeholder,
.form-textarea::placeholder {
- color: #3b5d7c;
+ color: var(--muted-foreground);
}
.form-textarea {
resize: vertical;
- min-height: 120px;
+ min-height: 140px;
line-height: 1.5;
}
@@ -131,24 +131,24 @@
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235a7aa3' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
- background-position: right 10px center;
- padding-right: 28px;
+ background-position: right 14px center;
+ padding-right: 36px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 12px;
+ gap: 16px;
}
/* ─── Error ──────────────────────────────────────────── */
.form-error {
- border: 1px solid rgba(255, 107, 157, 0.4);
- background: rgba(255, 107, 157, 0.08);
- color: #ff6b9d;
- padding: 8px 12px;
- font-size: 11px;
+ border: 1px solid rgba(var(--primary-rgb), 0.4);
+ background: rgba(var(--primary-rgb), 0.08);
+ color: var(--destructive);
+ padding: 10px 16px;
+ font-size: 13px;
letter-spacing: 0.08em;
}
@@ -157,19 +157,19 @@
.form-actions {
display: flex;
justify-content: flex-end;
- gap: 10px;
- padding-top: 4px;
+ gap: 12px;
+ padding-top: 6px;
}
.submit-btn {
- border-color: #4dd0e1 !important;
- color: #b8f2ff !important;
- background: rgba(77, 208, 225, 0.08) !important;
+ border-color: var(--primary) !important;
+ color: var(--foreground) !important;
+ background: rgba(var(--primary-rgb), 0.08) !important;
}
.submit-btn:hover:not(:disabled) {
- background: rgba(77, 208, 225, 0.15) !important;
- box-shadow: 0 0 12px rgba(77, 208, 225, 0.2);
+ background: rgba(var(--primary-rgb), 0.15) !important;
+ box-shadow: 0 0 12px rgba(var(--primary-rgb), 0.2);
}
.submit-btn:disabled {
@@ -181,10 +181,10 @@
.spinner {
display: inline-block;
- width: 12px;
- height: 12px;
- border: 2px solid rgba(77, 208, 225, 0.3);
- border-top-color: #4dd0e1;
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(var(--primary-rgb), 0.3);
+ border-top-color: var(--primary);
animation: spin 0.6s linear infinite;
}
diff --git a/client/src/components/InsightsPanel.css b/client/src/components/InsightsPanel.css
index 467fe2a..13d93c6 100644
--- a/client/src/components/InsightsPanel.css
+++ b/client/src/components/InsightsPanel.css
@@ -1,10 +1,10 @@
/* ─── Insights Panel ─────────────────────────────────── */
.insights {
- padding: 20px 16px 16px;
+ padding: 24px 20px 20px;
display: flex;
flex-direction: column;
- gap: 14px;
+ gap: 18px;
overflow-y: auto;
max-height: 100%;
animation: fadeIn 0.3s ease;
@@ -15,31 +15,31 @@
.insights-header {
display: flex;
align-items: center;
- gap: 8px;
+ gap: 10px;
flex-wrap: wrap;
}
.insights-type {
- font-size: 11px;
+ font-size: 13px;
letter-spacing: 0.16em;
text-transform: uppercase;
- color: #b8f2ff;
- padding: 3px 8px;
- border: 1px solid rgba(77, 208, 225, 0.3);
- background: rgba(77, 208, 225, 0.06);
+ color: var(--foreground);
+ padding: 4px 10px;
+ border: 1px solid rgba(var(--primary-rgb), 0.3);
+ background: rgba(var(--primary-rgb), 0.06);
}
.insights-severity {
- font-size: 9px;
+ font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
- padding: 2px 6px;
+ padding: 4px 8px;
border: 1px solid;
font-weight: 600;
}
.insights-confidence {
- font-size: 9px;
+ font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
margin-left: auto;
@@ -48,22 +48,22 @@
/* ─── Sections ───────────────────────────────────────── */
.insights-section {
- border-top: 1px solid #1e2d5f;
- padding-top: 10px;
+ border-top: 1px solid var(--border);
+ padding-top: 14px;
}
.insights-label {
- font-size: 10px;
+ font-size: 12px;
letter-spacing: 0.2em;
text-transform: uppercase;
- color: #4dd0e1;
- margin-bottom: 6px;
- text-shadow: 0 0 6px rgba(77, 208, 225, 0.3);
+ color: var(--primary);
+ margin-bottom: 8px;
+ text-shadow: 0 0 6px rgba(var(--primary-rgb), 0.3);
}
.insights-text {
- font-size: 12px;
- color: #8bb4d9;
+ font-size: 14px;
+ color: var(--foreground);
line-height: 1.6;
margin: 0;
}
@@ -72,43 +72,43 @@
.insights-list {
margin: 0;
- padding-left: 18px;
+ padding-left: 22px;
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 8px;
}
.insights-list li {
- font-size: 11px;
- color: #8bb4d9;
+ font-size: 13px;
+ color: var(--foreground);
line-height: 1.5;
}
.insights-list.ordered li::marker {
- color: #4dd0e1;
+ color: var(--primary);
}
.insights-evidence-item {
font-family: inherit;
- background: rgba(10, 14, 39, 0.6);
- border-left: 2px solid #3b5d7c;
- padding: 4px 8px;
- font-size: 11px;
- color: #6b8fb5;
+ background: var(--background);
+ border-left: 2px solid var(--muted-foreground);
+ padding: 6px 12px;
+ font-size: 13px;
+ color: var(--muted-foreground);
list-style: none;
}
/* ─── Sources ────────────────────────────────────────── */
.insights-source {
- color: #7bb3c8 !important;
+ color: var(--primary) !important;
}
.source-snippet {
display: block;
- font-size: 10px;
- color: #5a7aa3;
- margin-top: 2px;
+ font-size: 12px;
+ color: var(--muted-foreground);
+ margin-top: 4px;
line-height: 1.4;
}
@@ -120,51 +120,100 @@
align-items: center;
justify-content: center;
height: 100%;
- padding: 32px;
+ padding: 40px;
text-align: center;
- gap: 12px;
+ gap: 16px;
}
-.pulse-ring {
- width: 48px;
- height: 48px;
- border: 2px solid rgba(77, 208, 225, 0.3);
- position: relative;
- animation: pulseRing 1.5s ease-in-out infinite;
+.retro-terminal-loader {
+ background: var(--background);
+ border: 1px dashed var(--border);
+ font-family: 'Courier New', Courier, monospace;
}
-.pulse-ring::after {
- content: "";
- position: absolute;
- inset: 6px;
- border: 1px solid rgba(77, 208, 225, 0.5);
- animation: pulseRing 1.5s ease-in-out infinite 0.3s;
+.ascii-spinner {
+ color: var(--primary);
+ font-size: 10px;
+ line-height: 1.2;
+ margin: 0 0 20px 0;
+ text-shadow: 0 0 8px rgba(var(--primary-rgb), 0.4);
+ animation: loaderFlicker 0.15s infinite alternate;
}
-@keyframes pulseRing {
- 0%, 100% {
- opacity: 0.4;
- transform: scale(1);
- box-shadow: 0 0 0 rgba(77, 208, 225, 0);
- }
- 50% {
- opacity: 1;
- transform: scale(1.05);
- box-shadow: 0 0 20px rgba(77, 208, 225, 0.2);
- }
+.terminal-progress-info {
+ font-size: 15px;
+ color: var(--foreground);
+ letter-spacing: 0.1em;
+ margin-bottom: 10px;
+ text-shadow: 0 0 6px rgba(var(--primary-rgb), 0.4);
}
-.insights-loading-text {
- font-size: 12px;
- letter-spacing: 0.2em;
- text-transform: uppercase;
- color: #b8f2ff;
- margin: 0;
+.terminal-progress-bar {
+ font-size: 16px;
+ color: var(--primary);
+ letter-spacing: 0.15em;
+ background: var(--card);
+ padding: 10px 18px;
+ border: 1px solid var(--border);
+ border-radius: 2px;
+ text-shadow: 0 0 8px rgba(var(--primary-rgb), 0.4);
+}
+
+@keyframes loaderFlicker {
+ 0% { opacity: 0.95; }
+ 100% { opacity: 1; }
}
.insights-loading-sub {
- font-size: 10px;
- color: #5a7aa3;
+ font-size: 12px;
+ color: var(--muted-foreground);
letter-spacing: 0.12em;
margin: 0;
}
+
+/* ─── Resolution Action ─────────────────────────────── */
+
+.insights-resolve-section {
+ margin-top: 6px;
+ padding-top: 20px;
+}
+
+.resolve-btn {
+ border-color: var(--primary) !important;
+ color: var(--foreground) !important;
+ background: rgba(var(--primary-rgb), 0.06) !important;
+ padding: 14px 26px !important;
+ font-size: 14px !important;
+ letter-spacing: 0.14em;
+ transition:
+ background 0.2s,
+ box-shadow 0.2s,
+ border-color 0.2s;
+}
+
+.resolve-btn:hover {
+ background: rgba(var(--primary-rgb), 0.15) !important;
+ box-shadow:
+ 0 0 16px rgba(var(--primary-rgb), 0.25),
+ 0 0 0 1px rgba(var(--primary-rgb), 0.3);
+}
+
+.resolved-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 22px;
+ border: 1px solid rgba(46, 213, 115, 0.4);
+ background: rgba(46, 213, 115, 0.08);
+ color: #2ed573;
+ font-size: 14px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ font-weight: 600;
+ animation: fadeIn 0.3s ease;
+}
+
+.resolved-icon {
+ width: 18px;
+ height: 18px;
+}
diff --git a/client/src/components/InsightsPanel.jsx b/client/src/components/InsightsPanel.jsx
index 22b64c5..a2a7652 100644
--- a/client/src/components/InsightsPanel.jsx
+++ b/client/src/components/InsightsPanel.jsx
@@ -1,3 +1,4 @@
+import { useState, useEffect } from "react";
import "./InsightsPanel.css";
const CONFIDENCE_COLORS = {
@@ -6,17 +7,83 @@ const CONFIDENCE_COLORS = {
high: "#4dd0e1",
};
-export default function InsightsPanel({ result, loading }) {
- if (loading) {
- return (
-
-
-
Analyzing…
-
- Running AI analysis on selected log
-
+const STAGES = [
+ "INITIALIZING CORE AGENT...",
+ "ESTABLISHING INCIDENT CONTEXT...",
+ "PARSING LOG STACK TRACES...",
+ "RUNNING PROMPT INFERENCE...",
+ "RETRIEVING LATEST RAG RUNBOOKS...",
+ "COMPILING REMEDIES...",
+ "FINALIZING DIAGNOSTIC REPORT...",
+];
+
+function RetroLoader() {
+ const [progress, setProgress] = useState(0);
+ const [stage, setStage] = useState(0);
+
+ useEffect(() => {
+ const progressInterval = setInterval(() => {
+ setProgress((prev) => {
+ if (prev >= 100) {
+ clearInterval(progressInterval);
+ return 100;
+ }
+ const increment = Math.floor(Math.random() * 8) + 4;
+ const nextProgress = Math.min(prev + increment, 100);
+
+ const stageIndex = Math.min(
+ Math.floor((nextProgress / 100) * STAGES.length),
+ STAGES.length - 1,
+ );
+ setStage(stageIndex);
+
+ return nextProgress;
+ });
+ }, 250);
+
+ return () => clearInterval(progressInterval);
+ }, []);
+
+ const totalBars = 20;
+ const filledBars = Math.round((progress / 100) * totalBars);
+ const emptyBars = totalBars - filledBars;
+ const barString =
+ "=".repeat(Math.max(0, filledBars - 1)) +
+ (filledBars > 0 ? ">" : "") +
+ " ".repeat(emptyBars);
+
+ return (
+
+
+ {`
+ __ __ ____ ____ _ ___ _ _ ____
+ | \\/ |/ ___| / ___|| | |_ _| \\ | |/ ___|
+ | |\\/| | | _ \\___ \\| | | || \\| | | _
+ | | | | |_| | ___) | |___ | || |\\ | |_| |
+ |_| |_|\\____| |____/|_____|___|_| \\_|\\____|
+`}
+
+
+ > {STAGES[stage]}
- );
+
+ [{barString}] {progress}%
+
+
+ Please stand by. Core reasoning engine is processing log patterns.
+
+
+ );
+}
+
+export default function InsightsPanel({
+ result,
+ loading,
+ resolved,
+ onMarkResolved,
+}) {
+ if (loading) {
+ return
;
}
if (!result) return null;
@@ -117,6 +184,59 @@ export default function InsightsPanel({ result, loading }) {
)}
+
+ {/* ── Resolution Action ────────────────────────── */}
+
+ {resolved ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/client/src/components/LogList.css b/client/src/components/LogList.css
index e97c870..415dc9f 100644
--- a/client/src/components/LogList.css
+++ b/client/src/components/LogList.css
@@ -1,36 +1,113 @@
/* ─── Log List ───────────────────────────────────────── */
+.log-list-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ padding: 24px 20px 20px;
+}
+
+.log-search-container {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border: 1px solid var(--border);
+ background: var(--background);
+ padding: 10px 14px;
+ margin-bottom: 16px;
+ box-shadow: inset 0 0 10px rgba(var(--primary-rgb), 0.05);
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.log-search-container:focus-within {
+ border-color: var(--primary);
+ box-shadow:
+ 0 0 10px rgba(var(--primary-rgb), 0.15),
+ inset 0 0 10px rgba(var(--primary-rgb), 0.1);
+}
+
+.search-prompt {
+ color: var(--primary);
+ font-family: 'Courier New', Courier, monospace;
+ font-weight: bold;
+ font-size: 16px;
+ text-shadow: 0 0 5px rgba(var(--primary-rgb), 0.6);
+ animation: blinkCursor 1.2s step-end infinite;
+}
+
+@keyframes blinkCursor {
+ from, to { opacity: 0 }
+ 50% { opacity: 1; }
+}
+
+.log-search-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--foreground);
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 13px;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.log-search-input::placeholder {
+ color: var(--muted-foreground);
+}
+
+.clear-search-btn {
+ background: transparent;
+ border: none;
+ color: var(--muted-foreground);
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 12px;
+ cursor: pointer;
+ letter-spacing: 0.08em;
+ transition: color 0.2s;
+}
+
+.clear-search-btn:hover {
+ color: var(--destructive);
+}
+
+.no-matches-text {
+ padding: 32px 20px;
+ text-align: center;
+ color: var(--muted-foreground);
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 13px;
+ letter-spacing: 0.12em;
+}
+
.log-list {
display: flex;
flex-direction: column;
- gap: 8px;
- padding: 20px 16px 16px;
+ gap: 10px;
overflow-y: auto;
- max-height: 100%;
+ max-height: calc(100% - 56px);
}
.log-entry {
- border: 1px solid #2a3a6a;
- background: rgba(10, 14, 39, 0.6);
- padding: 10px 12px;
+ border: 1px solid var(--border);
+ background: var(--background);
+ padding: 14px 18px;
cursor: pointer;
- transition:
- border-color 0.2s,
- box-shadow 0.2s,
- background 0.2s;
+ transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.log-entry:hover {
- border-color: #3b5d7c;
- background: rgba(18, 24, 58, 0.8);
+ border-color: var(--primary);
+ background: var(--card);
+ box-shadow: 0 0 10px rgba(var(--primary-rgb), 0.12);
}
.log-entry.selected {
- border-color: rgba(77, 208, 225, 0.5);
+ border-color: var(--primary);
box-shadow:
- 0 0 0 1px rgba(77, 208, 225, 0.15),
- 0 0 16px rgba(77, 208, 225, 0.08);
- background: rgba(18, 24, 58, 0.95);
+ 0 0 0 1px rgba(var(--primary-rgb), 0.25),
+ 0 0 16px rgba(var(--primary-rgb), 0.15);
+ background: var(--card);
}
/* ─── Entry Header ───────────────────────────────────── */
@@ -38,51 +115,93 @@
.log-entry-header {
display: flex;
align-items: center;
- gap: 8px;
- margin-bottom: 6px;
+ gap: 10px;
+ margin-bottom: 8px;
}
.severity-badge {
- font-size: 9px;
+ font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
- padding: 2px 6px;
+ padding: 4px 8px;
border: 1px solid;
font-weight: 600;
white-space: nowrap;
}
.log-service {
- font-size: 12px;
- color: #b8f2ff;
+ font-size: 14px;
+ color: var(--foreground);
letter-spacing: 0.08em;
font-weight: 500;
}
.log-type {
- font-size: 9px;
- color: #5a7aa3;
+ font-size: 11px;
+ color: var(--muted-foreground);
letter-spacing: 0.12em;
text-transform: uppercase;
margin-left: auto;
}
+/* ─── Expand / Collapse Button ───────────────────────── */
+
+.expand-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--muted-foreground);
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition:
+ color 0.2s,
+ border-color 0.2s,
+ transform 0.25s ease;
+}
+
+.expand-btn:hover {
+ color: var(--primary);
+ border-color: var(--primary);
+}
+
+.expand-btn .chevron-icon {
+ width: 16px;
+ height: 16px;
+ transition: transform 0.25s ease;
+}
+
+.expand-btn.expanded .chevron-icon {
+ transform: rotate(180deg);
+}
+
/* ─── Content Preview ────────────────────────────────── */
.log-preview {
- font-size: 11px;
- color: #6b8fb5;
+ font-size: 13px;
+ color: var(--muted-foreground);
line-height: 1.4;
- max-height: 48px;
+ max-height: 56px;
overflow: hidden;
white-space: pre-wrap;
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;
}
@@ -92,23 +211,56 @@
display: flex;
align-items: center;
justify-content: space-between;
- margin-top: 8px;
+ margin-top: 10px;
}
.log-time {
- font-size: 10px;
- color: #5a7aa3;
+ font-size: 12px;
+ color: var(--muted-foreground);
letter-spacing: 0.1em;
}
.analyze-btn {
- border-color: #4dd0e1 !important;
- color: #b8f2ff !important;
+ border-color: var(--primary) !important;
+ color: var(--primary) !important;
animation: fadeIn 0.2s ease;
}
.analyze-btn:hover:not(:disabled) {
- box-shadow: 0 0 12px rgba(77, 208, 225, 0.3);
+ box-shadow: 0 0 12px rgba(var(--primary-rgb), 0.3);
+}
+
+.analyzed-badge {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 13px;
+ color: var(--primary);
+ letter-spacing: 0.1em;
+ padding: 6px 12px;
+ border: 1px solid rgba(var(--primary-rgb), 0.4);
+ background: rgba(var(--primary-rgb), 0.05);
+ text-shadow: 0 0 5px rgba(var(--primary-rgb), 0.4);
+ animation: fadeIn 0.2s ease;
+}
+
+.delete-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--muted-foreground);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all 0.2s ease;
+ font-size: 12px;
+ margin-left: 6px;
}
-/* Reuse spinner from IngestModal */
+.delete-btn:hover {
+ color: var(--destructive);
+ border-color: var(--destructive);
+ box-shadow: 0 0 8px rgba(255, 107, 157, 0.3);
+}
diff --git a/client/src/components/LogList.jsx b/client/src/components/LogList.jsx
index 5d6e8fe..e471d34 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",
@@ -13,84 +15,150 @@ export default function LogList({
onSelect,
onAnalyze,
analyzing,
+ onDelete,
}) {
+ const [searchQuery, setSearchQuery] = useState("");
+ 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;
+ });
+ };
+
+ const filteredLogs = logs.filter((log) => {
+ const query = searchQuery.toLowerCase().trim();
+ if (!query) return true;
+ return (
+ (log.serviceName && log.serviceName.toLowerCase().includes(query)) ||
+ (log.severity && log.severity.toLowerCase().includes(query)) ||
+ (log.type && log.type.toLowerCase().includes(query)) ||
+ (log.logContent && log.logContent.toLowerCase().includes(query))
+ );
+ });
+
return (
-
- {logs.map((log) => {
- const isSelected = selectedId === log.id;
- return (
-
onSelect(log.id)}
+
+
+
>
+
setSearchQuery(e.target.value)}
+ />
+ {searchQuery && (
+
+
+ );
+ })
+ )}
+
);
}
diff --git a/client/src/components/PrivacyToggle.css b/client/src/components/PrivacyToggle.css
index f31ea0a..4ff696d 100644
--- a/client/src/components/PrivacyToggle.css
+++ b/client/src/components/PrivacyToggle.css
@@ -4,15 +4,15 @@
display: flex;
flex-direction: column;
align-items: center;
- gap: 6px;
+ gap: 8px;
}
.privacy-toggle {
position: relative;
display: inline-flex;
align-items: stretch;
- border: 1px solid #2a3a6a;
- background: rgba(10, 14, 39, 0.85);
+ border: 1px solid var(--border);
+ background: var(--background);
overflow: hidden;
}
@@ -22,8 +22,8 @@
top: 2px;
bottom: 2px;
width: calc(50% - 2px);
- background: rgba(77, 208, 225, 0.12);
- border: 1px solid rgba(77, 208, 225, 0.35);
+ background: rgba(var(--primary-rgb), 0.12);
+ border: 1px solid rgba(var(--primary-rgb), 0.35);
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease;
@@ -34,15 +34,15 @@
.indicator-local {
transform: translateX(2px);
box-shadow:
- 0 0 12px rgba(77, 208, 225, 0.2),
- inset 0 0 8px rgba(77, 208, 225, 0.08);
+ 0 0 12px rgba(var(--primary-rgb), 0.2),
+ inset 0 0 8px rgba(var(--primary-rgb), 0.08);
}
.indicator-cloud {
transform: translateX(calc(100% + 2px));
box-shadow:
- 0 0 12px rgba(77, 208, 225, 0.2),
- inset 0 0 8px rgba(77, 208, 225, 0.08);
+ 0 0 12px rgba(var(--primary-rgb), 0.2),
+ inset 0 0 8px rgba(var(--primary-rgb), 0.08);
}
/* Individual toggle buttons */
@@ -51,12 +51,12 @@
z-index: 1;
display: inline-flex;
align-items: center;
- gap: 6px;
- padding: 7px 14px;
+ gap: 8px;
+ padding: 10px 18px;
border: none;
background: transparent;
- color: #5a7aa3;
- font-size: 11px;
+ color: var(--muted-foreground);
+ font-size: 13px;
letter-spacing: 0.16em;
text-transform: uppercase;
cursor: pointer;
@@ -67,54 +67,54 @@
}
.toggle-option:hover:not(.active) {
- color: #8bb4d9;
+ color: var(--foreground);
}
.toggle-option.active {
- color: #b8f2ff;
- text-shadow: 0 0 8px rgba(77, 208, 225, 0.5);
+ color: var(--foreground);
+ text-shadow: 0 0 8px rgba(var(--primary-rgb), 0.5);
}
.toggle-icon {
- width: 14px;
- height: 14px;
+ width: 16px;
+ height: 16px;
flex-shrink: 0;
transition: color 0.25s ease;
}
.toggle-option.active .toggle-icon {
- color: #4dd0e1;
- filter: drop-shadow(0 0 4px rgba(77, 208, 225, 0.6));
+ color: var(--primary);
+ filter: drop-shadow(0 0 4px rgba(var(--primary-rgb), 0.6));
}
/* Hint text below toggle */
.toggle-hint {
display: inline-flex;
align-items: center;
- gap: 6px;
- font-size: 10px;
+ gap: 8px;
+ font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
- color: #5a7aa3;
+ color: var(--muted-foreground);
transition: color 0.25s ease;
- min-height: 14px;
+ min-height: 16px;
}
.hint-dot {
- width: 5px;
- height: 5px;
+ width: 6px;
+ height: 6px;
display: inline-block;
transition: background 0.25s ease, box-shadow 0.25s ease;
}
.hint-dot--local {
- background: #4dd0e1;
- box-shadow: 0 0 6px rgba(77, 208, 225, 0.6);
+ background: var(--primary);
+ box-shadow: 0 0 6px rgba(var(--primary-rgb), 0.6);
}
.hint-dot--cloud {
- background: #ffd93d;
- box-shadow: 0 0 6px rgba(255, 217, 61, 0.5);
+ background: var(--chart-5);
+ box-shadow: 0 0 6px rgba(var(--primary-rgb), 0.5);
}
/* ─── Responsive ──────────────────────────────────────── */
@@ -125,6 +125,6 @@
}
.toggle-option {
- padding: 7px 10px;
+ padding: 10px 14px;
}
}
diff --git a/client/src/components/PrivacyToggle.jsx b/client/src/components/PrivacyToggle.jsx
index d2d6e47..2cf0ab1 100644
--- a/client/src/components/PrivacyToggle.jsx
+++ b/client/src/components/PrivacyToggle.jsx
@@ -1,5 +1,6 @@
import { usePrivacyMode } from "@/context/PrivacyModeContext";
import "./PrivacyToggle.css";
+import { LocalIcon, CloudIcon } from "@/components/icons";
export default function PrivacyToggle() {
const { setMode, isLocal } = usePrivacyMode();
@@ -26,31 +27,7 @@ export default function PrivacyToggle() {
onClick={() => setMode("local")}
id="privacy-toggle-local"
>
-
+
Local Only
@@ -63,20 +40,7 @@ export default function PrivacyToggle() {
onClick={() => setMode("cloud")}
id="privacy-toggle-cloud"
>
-
+
Cloud Expert
diff --git a/client/src/components/ResolveModal.css b/client/src/components/ResolveModal.css
new file mode 100644
index 0000000..c3b6a7f
--- /dev/null
+++ b/client/src/components/ResolveModal.css
@@ -0,0 +1,101 @@
+/* ─── Resolve Modal ──────────────────────────────────── */
+
+.resolve-modal {
+ max-width: 600px;
+}
+
+.resolve-body {
+ padding: 24px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+/* ─── Issue Context ──────────────────────────────────── */
+
+.resolve-context {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ border: 1px solid var(--border);
+ background: var(--background);
+}
+
+.resolve-context-label {
+ font-size: 12px;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--muted-foreground);
+}
+
+.resolve-context-value {
+ font-size: 13px;
+ color: var(--foreground);
+ letter-spacing: 0.06em;
+}
+
+/* ─── Sections ───────────────────────────────────────── */
+
+.resolve-section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.resolve-section-label {
+ font-size: 13px;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--primary);
+ text-shadow: 0 0 6px rgba(var(--primary-rgb), 0.3);
+}
+
+.resolve-section-desc {
+ font-size: 13px;
+ color: var(--muted-foreground);
+ line-height: 1.5;
+ margin: 0;
+}
+
+/* ─── Simple Resolve Button ──────────────────────────── */
+
+.resolve-simple-btn {
+ align-self: flex-start;
+ border-color: var(--border) !important;
+}
+
+.resolve-simple-btn:hover:not(:disabled) {
+ border-color: var(--primary) !important;
+ color: var(--foreground) !important;
+ box-shadow: 0 0 10px rgba(var(--primary-rgb), 0.15);
+}
+
+/* ─── RAG Submit Button ──────────────────────────────── */
+
+.resolve-rag-btn {
+ align-self: flex-start;
+}
+
+/* ─── Divider ────────────────────────────────────────── */
+
+.resolve-divider {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.resolve-divider::before,
+.resolve-divider::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: var(--border);
+}
+
+.resolve-divider-text {
+ font-size: 12px;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ color: var(--muted-foreground);
+}
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.
+
+
+ {submitting ? (
+ <>
+
+ Resolving…
+ >
+ ) : (
+ "Mark as Resolved"
+ )}
+
+
+
+ {/* ── Divider ───────────────────────────────── */}
+
+ or
+
+
+ {/* ── Option B: RAG Feedback ────────────────── */}
+
+
+ {/* ── Error ─────────────────────────────────── */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ );
+}
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";
diff --git a/client/src/index.css b/client/src/index.css
index f829f92..5aa6b55 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -36,6 +36,64 @@
--sidebar-accent-foreground: #8bb4d9;
--sidebar-border: #2a3a6a;
--sidebar-ring: #4dd0e1;
+ --border: #2a3a6a;
+ --primary-rgb: 77, 208, 225;
+}
+
+/* Classic Phosphor Green Theme */
+.theme-green {
+ --background: #050a05;
+ --foreground: #33ff33;
+ --card: #0c180c;
+ --card-foreground: #a3ffa3;
+ --popover: #0f220f;
+ --popover-foreground: #a3ffa3;
+ --primary: #33ff33;
+ --primary-foreground: #050a05;
+ --secondary: #163316;
+ --secondary-foreground: #a3ffa3;
+ --muted: #0f220f;
+ --muted-foreground: #22aa22;
+ --destructive: #ff4d4d;
+ --ring: #33ff33;
+ --sidebar: #0c180c;
+ --sidebar-foreground: #a3ffa3;
+ --sidebar-primary: #33ff33;
+ --sidebar-primary-foreground: #050a05;
+ --sidebar-accent: #163316;
+ --sidebar-accent-foreground: #a3ffa3;
+ --sidebar-border: #1a3c1a;
+ --sidebar-ring: #33ff33;
+ --border: #1a3c1a;
+ --primary-rgb: 51, 255, 51;
+}
+
+/* Monochrome Phosphor Amber Theme */
+.theme-amber {
+ --background: #0a0600;
+ --foreground: #ffb000;
+ --card: #160d00;
+ --card-foreground: #ffd580;
+ --popover: #1f1200;
+ --popover-foreground: #ffd580;
+ --primary: #ffb000;
+ --primary-foreground: #0a0600;
+ --secondary: #301b00;
+ --secondary-foreground: #ffd580;
+ --muted: #1f1200;
+ --muted-foreground: #b37d00;
+ --destructive: #ff5500;
+ --ring: #ffb000;
+ --sidebar: #160d00;
+ --sidebar-foreground: #ffd580;
+ --sidebar-primary: #ffb000;
+ --sidebar-primary-foreground: #0a0600;
+ --sidebar-accent: #301b00;
+ --sidebar-accent-foreground: #ffd580;
+ --sidebar-border: #442600;
+ --sidebar-ring: #ffb000;
+ --border: #442600;
+ --primary-rgb: 255, 176, 0;
}
@theme inline {
diff --git a/client/src/lib/api.js b/client/src/lib/api.js
index 490d537..2dbc2e8 100644
--- a/client/src/lib/api.js
+++ b/client/src/lib/api.js
@@ -85,3 +85,32 @@ export async function checkHealth() {
}
return response.json();
}
+
+/**
+ * Submit a custom solution document to the RAG knowledge base.
+ *
+ * Maps to POST /api/v1/rag/documents (see OpenAPI spec).
+ *
+ * @param {string} title – Document title (e.g. problem type or summary).
+ * @param {string} content – The custom solution / root-cause fix text.
+ * @param {string[]} [tags] – Optional tags for categorization.
+ * @returns {Promise