diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png index d1260ef..c5afad7 100644 Binary files a/docs/screenshots/overview.png and b/docs/screenshots/overview.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c7e1190..d8f63ab 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,191 +1,56 @@ import { useEffect, useState } from "react"; import DocumentLibrary from "./DocumentLibrary"; import AskContextIQ from "./AskContextIQ"; -import "./App.css"; -import "./phase2.css"; +import Overview from "./Overview"; import Login from "./Login"; import { apiFetch } from "./api"; +import "./App.css"; +import "./phase2.css"; -type Readiness = { status: string; checks: Record }; +type Page = "overview" | "documents" | "ask"; export default function App() { - const [page, setPage] = useState(() => { + const [page, setPage] = useState(() => { const saved = window.location.hash.replace("#", ""); return saved === "documents" || saved === "ask" ? saved : "overview"; }); - const [api, setApi] = useState("checking"); - const [readiness, setReadiness] = useState(null); - const [auth, setAuth] = useState<"checking" | "authenticated" | "anonymous">( - "checking", - ); - - useEffect(() => { - fetch("/api/health") - .then((response) => { - if (!response.ok) throw new Error("API error"); - return response.json(); - }) - .then(() => setApi("online")) - .catch(() => setApi("offline")); - fetch("/api/ready") - .then((response) => { - if (!response.ok) throw new Error("Readiness error"); - return response.json(); - }) - .then(setReadiness) - .catch(() => setReadiness(null)); - }, []); + const [auth, setAuth] = useState<"checking" | "authenticated" | "anonymous">("checking"); + // Session verification runs once on application initialization. + // The overview loads its own live API data when it becomes visible. useEffect(() => { - fetch("/api/auth/me") - .then((response) => { - setAuth(response.ok ? "authenticated" : "anonymous"); - }) + void fetch("/api/auth/me", { credentials: "same-origin" }) + .then((response) => setAuth(response.ok ? "authenticated" : "anonymous")) .catch(() => setAuth("anonymous")); }, []); - if (auth === "checking") { - return
Checking session...
; + function navigate(next: Page) { + setPage(next); + window.location.hash = next; } - if (auth === "anonymous") { - return setAuth("authenticated")} />; - } + if (auth === "checking") return
Checking session...
; + if (auth === "anonymous") return setAuth("authenticated")} />; return (
- {page === "documents" ? ( - - ) : page === "ask" ? ( - - ) : ( - <> -
KNOWLEDGE INTELLIGENCE / OVERVIEW
-

- Your knowledge, connected. -

-

- A private, local-first workspace for evidence-grounded document - intelligence. -

-
-
- Document intelligence -

- Phase 3 adds semantic document retrieval and source-grounded - answers with Ollama. -

-
- PHASE 03 / 07 -
-

System status

-
- - - -
-
-
▤
-

Document library is ready

-

- Choose Documents in the sidebar to upload and inspect your PDFs. -

- PDF UPLOAD · PHASE 2 -
-
ContextIQ v0.3.0 · Local development environment
- - )} + {page === "documents" ? : page === "ask" ? : }
); } - -function StatusCard({ - name, - detail, - status, -}: { - name: string; - detail: string; - status: string; -}) { - const healthy = status === "ok" || status === "online"; - return ( -
-
- ◈ - - {healthy ? "Connected" : status} - -
- {name} -

{detail}

-
- ); -} diff --git a/frontend/src/Overview.tsx b/frontend/src/Overview.tsx new file mode 100644 index 0000000..c0a8baa --- /dev/null +++ b/frontend/src/Overview.tsx @@ -0,0 +1,212 @@ +import { useCallback, useEffect, useState } from "react"; +import "./overview.css"; + +type Readiness = { status: string; checks: Record }; +type DocumentItem = { + id: string; + filename: string; + page_count: number; + status: string; + created_at: string; +}; +type IndexedItem = { + document_id: string; + filename: string; + chunks: number; +}; +type OverviewProps = { onNavigate: (page: "documents" | "ask") => void }; + +type DashboardData = { + documents: DocumentItem[] | null; + index: IndexedItem[] | null; + api: "online" | "offline"; + readiness: Readiness | null; +}; + +const initial: DashboardData = { + documents: null, + index: null, + api: "offline", + readiness: null, +}; + +async function fetchJson(url: string): Promise { + const response = await fetch(url, { + credentials: "same-origin", + cache: "no-store", + }); + if (!response.ok) throw new Error(`Request failed: ${response.status}`); + return response.json() as Promise; + } + +export default function Overview({ onNavigate }: OverviewProps) { + const [data, setData] = useState(initial); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [lastUpdated, setLastUpdated] = useState(null); + + const refresh = useCallback(async (signal?: AbortSignal) => { + setRefreshing(true); + const [health, readiness, documents, index] = await Promise.allSettled([ + fetchJson<{ status: string }>("/api/health"), + fetchJson("/api/ready"), + fetchJson("/api/documents"), + fetchJson("/api/rag/status"), + ]); + if (signal?.aborted) return; + setData({ + api: health.status === "fulfilled" ? "online" : "offline", + readiness: readiness.status === "fulfilled" ? readiness.value : null, + documents: documents.status === "fulfilled" ? documents.value : null, + index: index.status === "fulfilled" ? index.value : null, + }); + setLastUpdated(new Date()); + setLoading(false); + setRefreshing(false); + }, []); + + useEffect(() => { + const controller = new AbortController(); + void refresh(controller.signal); + return () => controller.abort(); + }, [refresh]); + + const documentCount = data.documents?.length; + const indexedCount = data.index?.filter((item) => item.chunks > 0).length; + const chunkCount = data.index?.reduce((sum, item) => sum + item.chunks, 0); + const recentDocuments = [...(data.documents ?? [])] + .sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)) + .slice(0, 4); + + const apiHealthy = data.api === "online"; + const dbHealthy = data.readiness?.checks.database === "ok"; + const blobHealthy = data.readiness?.checks.blob_emulator === "ok"; + const allHealthy = apiHealthy && dbHealthy && blobHealthy; + const healthKnown = !loading && data.readiness !== null; + + return ( +
+
+
KNOWLEDGE INTELLIGENCE / OVERVIEW
+ +
+

Your knowledge, connected.

+

+ Private document intelligence with local AI, grounded retrieval, and transparent system health. +

+ +
+
+ LOCAL AI WORKSPACE +

Turn documents into answers.

+

Index text-based PDFs, ask evidence-grounded questions, and trace the source of every retrieved passage.

+
+ + +
+
+
+ ); +} + +function StatCard({ label, value, loading, detail, icon }: { + label: string; + value: number | undefined; + loading: boolean; + detail: string; + icon: string; +}) { + return ( +
+
{label}
+ {loading ? "…" : value === undefined ? "—" : value.toLocaleString()} +

{value === undefined && !loading ? "Data unavailable" : detail}

+
+ ); +} + +function ServiceRow({ label, detail, state }: { + label: string; + detail: string; + state: "ok" | "checking" | "unavailable"; +}) { + return ( +
+
+ ); +} diff --git a/frontend/src/overview.css b/frontend/src/overview.css new file mode 100644 index 0000000..503092e --- /dev/null +++ b/frontend/src/overview.css @@ -0,0 +1,14 @@ +.overview-page{--ov-text:#eaf1ff;--ov-muted:#91a4be;--ov-border:#273a55;--ov-card:#101d2f;color:var(--ov-text)} +.overview-topline{display:flex;align-items:center;justify-content:space-between;gap:16px} +.overview-refresh{border:1px solid #2a4264;border-radius:9px;background:#11233b;color:#b6d0f7;padding:9px 13px;font-size:12px;cursor:pointer} +.overview-refresh:disabled{opacity:.6;cursor:wait}.overview-refresh:hover:not(:disabled){background:#1b3555} +.overview-intro{max-width:680px;margin-bottom:30px!important;line-height:1.65} +.overview-hero{display:flex;align-items:center;justify-content:space-between;gap:28px;min-height:245px;padding:30px 36px;border:1px solid #36547a;border-radius:17px;background:radial-gradient(circle at 81% 48%,#204b83 0%,#142b49 31%,#101f34 68%);overflow:hidden;position:relative} +.overview-hero-copy{position:relative;z-index:1;flex:1;max-width:650px}.overview-kicker{color:#8fb6f7;font-size:10px;font-weight:800;letter-spacing:2.1px;display:flex;align-items:center;gap:9px}.overview-spark{display:block;width:7px;height:7px;border-radius:50%;background:#58d9ab;box-shadow:0 0 14px #58d9ab} +.overview-hero h2{font-size:clamp(23px,2.7vw,33px);letter-spacing:-1px;margin:17px 0 7px}.overview-hero p{font-size:13px;line-height:1.8;color:#a8bcd4;max-width:520px;margin:0}.overview-actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}.overview-actions button{cursor:pointer;border-radius:8px;padding:11px 17px;font-weight:700;font-size:12px;display:inline-flex;gap:15px;align-items:center}.overview-primary{border:1px solid #6b9cff;background:#467ef4;color:white}.overview-primary:hover{background:#5a90ff}.overview-secondary{border:1px solid #3b5578;color:#c9defc;background:#193351}.overview-secondary:hover{background:#264669} +.overview-hero-visual{height:200px;width:235px;flex-shrink:0;position:relative;display:grid;place-items:center}.overview-orbit{position:absolute;border:1px solid #6899dd64;border-radius:50%;inset:11px 28px;transform:rotate(-23deg)}.overview-orbit.orbit-inner{inset:40px 57px;transform:rotate(35deg);border-color:#91b6ff80}.overview-core{width:75px;height:75px;display:grid;place-items:center;border-radius:19px;background:linear-gradient(135deg,#3f72e9,#224784);box-shadow:0 0 55px #467ef450;color:white;font-weight:850;font-size:27px}.overview-orbit-label{position:absolute;font-size:8px;color:#b9cff0;letter-spacing:2px;font-weight:700}.overview-orbit-label.label-top{top:8px;right:0}.overview-orbit-label.label-bottom{bottom:11px;left:3px} +.overview-section-heading{display:flex;align-items:end;justify-content:space-between;gap:12px;margin-top:38px;margin-bottom:16px}.overview-section-eyebrow{font-size:10px;color:#7baffb;font-weight:800;letter-spacing:1.65px}.overview-section-heading h2,.overview-panel-head h2{font-size:20px;margin:7px 0 0}.overview-updated{color:#758ba7;font-size:11px}.overview-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-stat{min-width:0;border:1px solid var(--ov-border);background:linear-gradient(165deg,#14243b,#101b2b);border-radius:13px;padding:20px 22px}.overview-stat-top{display:flex;align-items:center;justify-content:space-between;color:#a1b4cf;font-size:12px}.overview-stat-icon{font-size:23px;color:#8db5ff}.overview-stat strong{display:block;font-size:32px;letter-spacing:-1px;margin-top:9px}.overview-stat p{color:#718bab;font-size:11px;margin:5px 0 0} +.overview-lower-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:22px}.overview-panel{background:#101c2c;border:1px solid var(--ov-border);border-radius:14px;padding:23px;min-height:300px;min-width:0}.overview-panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:15px}.overview-panel-head h2{font-size:18px}.overview-health-tag{border-radius:30px;background:#372e29;color:#ddba85;padding:6px 9px;font-size:10px;white-space:nowrap}.overview-health-tag.healthy{background:#173d35;color:#71ddb3}.overview-service-row{display:flex;align-items:center;gap:12px;padding:13px 0;border-bottom:1px solid #243449}.overview-service-light{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:#b38c60}.overview-service-light.ok{background:#4dd7a4;box-shadow:0 0 8px #4dd7a444}.overview-service-light.checking{background:#b8a06c}.overview-service-row>div{display:flex;flex-direction:column;gap:5px;flex:1;min-width:0}.overview-service-row strong{font-size:12px}.overview-service-row div span{color:#8197b4;font-size:11px}.overview-service-state{font-size:10px;color:#dfac79}.overview-service-state.ok{color:#63dbaf}.overview-panel-foot{margin:14px 0 0;color:#7188a4;font-size:10px;line-height:1.5} +.overview-text-link{background:none;border:0;color:#88b8ff;cursor:pointer;font-size:12px;padding:6px}.overview-text-link:hover{text-decoration:underline}.overview-recent-list{display:grid;gap:0}.overview-recent-item{display:flex;gap:11px;align-items:center;padding:13px 0;border-bottom:1px solid #243449}.overview-file-symbol{display:grid;place-items:center;flex-shrink:0;width:37px;height:39px;background:#1b3554;border:1px solid #355985;border-radius:8px;color:#a8ceff;font-size:10px;font-weight:800}.overview-file-info{min-width:0;display:flex;flex-direction:column;gap:5px}.overview-file-info strong{font-size:12px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.overview-file-info span{font-size:11px;color:#8197b4}.overview-empty-library{text-align:center;padding:31px 10px}.overview-empty-icon{font-size:34px;color:#8cbaff}.overview-empty-library strong{display:block;margin-top:8px}.overview-empty-library p,.overview-empty-note{color:#8ca3bf;font-size:12px;line-height:1.7}.overview-empty-library button{border:0;background:none;color:#91bdff;font-weight:700;cursor:pointer;font-size:12px;margin-top:5px} +@media(max-width:1030px){.overview-hero-visual{width:145px;height:150px}.overview-orbit{inset:8px 8px}.overview-orbit.orbit-inner{inset:30px 30px}.overview-orbit-label{display:none}.overview-lower-grid{grid-template-columns:1fr}} +@media(max-width:700px){.overview-hero{padding:25px}.overview-hero-visual{display:none}.overview-stats{grid-template-columns:1fr}.overview-topline{align-items:flex-start}.overview-panel{padding:18px}.overview-updated{display:none}}