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 ( -
+
>_
-

DevPulse

-

Intelligent Logbook v1.0.0

+

DEVPULSE

+

INTELLIGENT LOGBOOK // SYSTEM_ONLINE

@@ -79,36 +147,23 @@ function App() { id="btn-ingest" onClick={() => setShowIngestModal(true)} > - + Ingest Logs + {hasLogs && ( + + )}
@@ -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 && ( +
+ + {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 ( -
- )} + + {/* ── Resolution Action ────────────────────────── */} +
+ {resolved ? ( +
+ + 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 && ( + + +
-
{log.logContent}
+
+									{log.logContent}
+								
-
- - {new Date(log.timestamp).toLocaleString()} - - {isSelected && ( - + ) )} - - )} -
-
- ); - })} +
+
+ ); + }) + )} +
); } 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. +

+ +
+ + {/* ── 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. +

+
+ +