Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 176 additions & 95 deletions client/src/App.css

Large diffs are not rendered by default.

224 changes: 139 additions & 85 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,64 @@ 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;

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);
Expand All @@ -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 {
Expand All @@ -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 (
<div className="page">
<div className={`page theme-${theme}`}>
<div className="scanlines" aria-hidden="true"></div>
<div className="frame">
<header className="topbar">
<div className="brand">
<div className="brand-mark">&gt;_</div>
<div>
<h1 className="brand-title">DevPulse</h1>
<p className="brand-sub">Intelligent Logbook v1.0.0</p>
<h1 className="brand-title">DEVPULSE</h1>
<p className="brand-sub">INTELLIGENT LOGBOOK // SYSTEM_ONLINE</p>
</div>
</div>
<div className="actions">
Expand All @@ -79,36 +147,23 @@ function App() {
id="btn-ingest"
onClick={() => setShowIngestModal(true)}
>
<svg
className="ghost-icon"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
d="M12 4v9"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
<path
d="M8.5 7.5L12 4l3.5 3.5"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
<rect
x="5"
y="13"
width="14"
height="7"
rx="1"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
<IngestIcon />
Ingest Logs
</button>
{hasLogs && (
<button
type="button"
className="ghost-btn"
id="btn-clear"
onClick={() => {
setLogs([]);
setSelectedLogId(null);
setNotification("All logs cleared");
}}
>
Clear Logs
</button>
)}
<PrivacyToggle />
</div>
</header>
Expand All @@ -125,34 +180,12 @@ function App() {
onSelect={handleSelectLog}
onAnalyze={handleAnalyze}
analyzing={analyzing}
onDelete={handleDelete}
/>
</div>
) : (
<div className="panel-body empty">
<svg
className="empty-icon"
viewBox="0 0 64 64"
aria-hidden="true"
>
<path
d="M18 8h20l10 10v30a8 8 0 0 1-8 8H18a8 8 0 0 1-8-8V16a8 8 0 0 1 8-8z"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M38 8v12h12"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M20 34h24M20 42h18"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
</svg>
<EmptyLogsIcon />
<p className="empty-title">No Logs Ingested</p>
<button
type="button"
Expand All @@ -172,36 +205,18 @@ function App() {
<div className="panel-body">
<InsightsPanel loading={true} result={null} />
</div>
) : analysisResult ? (
) : currentAnalysisResult ? (
<div className="panel-body">
<InsightsPanel loading={false} result={analysisResult} />
<InsightsPanel
loading={false}
result={currentAnalysisResult}
resolved={isCurrentLogResolved}
onMarkResolved={() => setShowResolveModal(true)}
/>
</div>
) : (
<div className="panel-body empty">
<svg
className="empty-icon"
viewBox="0 0 64 64"
aria-hidden="true"
>
<path
d="M22 26a10 10 0 0 1 20 0c0 7-6 8-6 14H28c0-6-6-7-6-14z"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M26 44h12M24 50h16"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M14 28h6M44 28h6M32 12v6"
fill="none"
stroke="currentColor"
strokeWidth="3"
/>
</svg>
<EmptyInsightsIcon />
{analysisError ? (
<p className="empty-title error-text">{analysisError}</p>
) : (
Expand Down Expand Up @@ -232,6 +247,15 @@ function App() {
<span className="mode-indicator">
Mode: {mode === "local" ? "🔒 Local" : "☁️ Cloud"}
</span>
<span className="divider"></span>
<button
type="button"
className="theme-btn"
onClick={() => setTheme((prev) => prev === "cyan" ? "green" : prev === "green" ? "amber" : "cyan")}
aria-label="Cycle UI theme"
>
🎨 Theme: {theme.toUpperCase()}
</button>
</div>
<div className="status-right">{clock.toLocaleString()}</div>
</footer>
Expand All @@ -244,6 +268,36 @@ function App() {
onClose={() => setShowIngestModal(false)}
/>
)}

{/* ── Resolve Modal ─────────────────────────────── */}
{showResolveModal && (
<ResolveModal
result={currentAnalysisResult}
onResolve={handleResolve}
onClose={() => setShowResolveModal(false)}
/>
)}

{/* ── Success Notification Toast ────────────────── */}
{notification && (
<div className="toast" role="status" aria-live="polite">
<svg
className="toast-icon"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<path
d="M20 6L9 17l-5-5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{notification}
</div>
)}
</div>
);
}
Expand Down
Loading
Loading