RemanenetSpy
fix(ui): mobile-responsive layout for all dashboard pages — Agent, Overview, Ingest, Query
21c5eec
Raw
History Blame Contribute Delete
11.6 kB
"use client";
import { useState, useEffect } from "react";
import { apiCall } from "@/lib/api";
import { Key, Activity, Search, Cpu, Hexagon, ChevronRight, ChevronDown, ArrowRight } from "lucide-react";
// ── Steps ──────────────────────────────────────────────────────────────────────
const STEPS = [
{
id: "keys",
Icon: Key,
title: "Get your API Key",
section: "API Keys",
description: "Go to API Keys in the sidebar. Generate your first key — attach it as the Authorization header on every request.",
code: `curl -H "Authorization: Bearer YOUR_KEY" \\
https://spy9191-chronos-api-backend.hf.space/health`,
},
{
id: "ingest",
Icon: Activity,
title: "Ingest your first event",
section: "Ingest Events",
description: "Send raw text. Smriti extracts Subject-Verb-Object tuples and stores them with timestamps automatically.",
code: `POST /ingest
{ "source_id": "my-app", "events": [{"text": "Acme Corp signed a $50k contract"}] }`,
},
{
id: "query",
Icon: Search,
title: "Query your memory",
section: "Query Memory",
description: "Ask natural-language questions. Smriti runs hybrid temporal + semantic search and returns ranked results.",
code: `POST /query
{ "query": "What happened with Acme Corp?" }`,
},
{
id: "agent",
Icon: Cpu,
title: "Run an Agent",
section: "Agent Chat",
description: "A LangGraph agent with your entire memory as context. Ask complex questions or trigger tools — it reasons across time.",
code: `POST /agent/run
{ "prompt": "Summarise all Q2 deals and flag any risks" }`,
},
{
id: "connect",
Icon: Hexagon,
title: "Connect a tool",
section: "Connect Tool",
description: "Register SaaS tools (Stripe, Notion, CRM). The Agent calls them mid-conversation and stores results as new events.",
code: `POST /connectors/register
{ "name": "Stripe", "base_url": "https://api.stripe.com", "endpoints": [...] }`,
},
];
const ACTIONS = [
{ id: "ingest", Icon: Activity, label: "Ingest an Event", sub: "Send text → Smriti stores it as structured memory" },
{ id: "query", Icon: Search, label: "Query Memory", sub: "Ask a question → get ranked results across all events" },
{ id: "agent", Icon: Cpu, label: "Run Agent", sub: "Let the AI reason across your full memory context" },
];
function QuickStart() {
const [open, setOpen] = useState(false);
const [openStep, setOpenStep] = useState<string | null>(null);
const [done, setDone] = useState<Set<string>>(new Set());
const toggleDone = (id: string, e: React.MouseEvent | React.KeyboardEvent) => {
e.stopPropagation();
setDone((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
const pct = Math.round((done.size / STEPS.length) * 100);
return (
<div className="border border-[#eaeaea] rounded-xl bg-white overflow-hidden">
<button
onClick={() => setOpen((o) => !o)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[#fafafa] transition-colors text-left"
>
<div className="flex items-center gap-2 flex-1 min-w-0">
{open
? <ChevronDown className="w-3.5 h-3.5 text-[#999] flex-shrink-0" />
: <ChevronRight className="w-3.5 h-3.5 text-[#999] flex-shrink-0" />
}
<span className="text-sm font-medium text-black">Quick Start</span>
<span className="text-xs text-[#aaa]">{done.size}/{STEPS.length} steps</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="w-16 md:w-20 h-1.5 bg-[#e8e8e8] rounded-full overflow-hidden">
<div className="h-full bg-black rounded-full transition-all duration-500" style={{ width: `${pct}%` }} />
</div>
<span className="text-[10px] text-[#999] font-medium w-6 text-right">{pct}%</span>
</div>
</button>
{open && (
<div className="border-t border-[#f0f0f0] divide-y divide-[#f5f5f5]">
{STEPS.map((step) => {
const isOpen = openStep === step.id;
const isDone = done.has(step.id);
const { Icon } = step;
return (
<div key={step.id}>
<button
onClick={() => setOpenStep(isOpen ? null : step.id)}
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-[#fafafa] transition-colors text-left"
>
<div
role="checkbox" aria-checked={isDone} tabIndex={0}
onClick={(e) => toggleDone(step.id, e)}
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") toggleDone(step.id, e as any); }}
className={`flex-shrink-0 w-4 h-4 rounded border flex items-center justify-center transition-colors cursor-pointer ${
isDone ? "bg-black border-black" : "border-[#d0d0d0] hover:border-black"
}`}
>
{isDone && (
<svg width="8" height="6" viewBox="0 0 10 8" fill="none">
<path d="M1 4L3.5 6.5L9 1" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</div>
<Icon className={`w-3.5 h-3.5 flex-shrink-0 ${isDone ? "text-[#ccc]" : "text-black"}`} strokeWidth={1.5} />
<span className={`text-sm flex-1 ${isDone ? "text-[#bbb] line-through" : "text-black"}`}>{step.title}</span>
<span className="text-[10px] text-[#ccc] uppercase tracking-wider hidden sm:block">{step.section}</span>
<ChevronDown className={`w-3 h-3 text-[#ccc] transition-transform flex-shrink-0 ${isOpen ? "rotate-180" : ""}`} />
</button>
{isOpen && (
<div className="px-4 pb-4 ml-10 space-y-2">
<p className="text-xs text-[#666] leading-relaxed">{step.description}</p>
<div className="bg-[#fafafa] border border-[#eaeaea] rounded-md p-3 font-mono text-[11px] text-black overflow-x-auto">
<pre>{step.code}</pre>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}
export function Overview({ apiKey, onNavigate }: { apiKey: string; onNavigate?: (page: string) => void }) {
const [health, setHealth] = useState<any>(null);
const [error, setError] = useState("");
useEffect(() => {
async function fetchHealth() {
try { const data = await apiCall("GET", "/health", apiKey); setHealth(data); }
catch (e: any) { setError(e.message); }
}
fetchHealth();
}, [apiKey]);
return (
<div className="max-w-4xl mx-auto px-4 md:px-8 py-6 md:py-8 space-y-5">
{/* Header */}
<div>
<h1 className="text-xl font-semibold text-black">Overview</h1>
<p className="text-sm text-[#999] mt-0.5">Smriti by Kaal the Absolute.</p>
</div>
{/* No API key */}
{!apiKey && (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 px-4 py-3 bg-[#fafafa] border border-[#e0e0e0] rounded-lg">
<div className="flex items-center gap-2.5">
<div className="w-1.5 h-1.5 rounded-full bg-[#f59e0b] flex-shrink-0" />
<span className="text-sm text-black font-medium">No API key set</span>
<span className="text-sm text-[#999] hidden sm:inline">— enter yours to connect Smriti</span>
</div>
<button
onClick={() => onNavigate?.("keys")}
className="text-xs font-medium text-black underline underline-offset-2 hover:opacity-60 transition-opacity self-start sm:self-auto"
>
Go to API Keys →
</button>
</div>
)}
{/* Stats — 2-col on mobile, 4-col on desktop */}
{error ? (
<div className="bg-red-50 text-red-700 px-4 py-3 rounded-lg border border-red-200 text-sm">⚠ {error}</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="border border-[#eaeaea] rounded-lg p-4 bg-white hover:border-black transition-colors">
<div className="text-2xl font-semibold text-black">
{health ? (health.stores?.postgres_events || 0).toLocaleString() : <span className="text-[#ddd]"></span>}
</div>
<div className="text-[10px] uppercase tracking-wider text-[#999] mt-1.5 font-medium">Events</div>
</div>
<div className="border border-[#eaeaea] rounded-lg p-4 bg-white hover:border-black transition-colors">
<div className="text-2xl font-semibold text-black">
{health ? (health.stores?.pgvector_embeddings || 0).toLocaleString() : <span className="text-[#ddd]"></span>}
</div>
<div className="text-[10px] uppercase tracking-wider text-[#999] mt-1.5 font-medium">Embeddings</div>
</div>
<div className="border border-[#eaeaea] rounded-lg p-4 bg-white hover:border-black transition-colors">
<div className="flex items-center gap-1.5">
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${health ? "bg-[#0a8f44]" : "bg-[#ddd] animate-pulse"}`} />
<span className="text-sm font-medium text-black">{health ? "Online" : "…"}</span>
</div>
<div className="text-[10px] uppercase tracking-wider text-[#999] mt-1.5 font-medium">Status</div>
</div>
<div className="border border-[#eaeaea] rounded-lg p-4 bg-white hover:border-black transition-colors">
<div className="text-sm font-medium text-black">120B</div>
<div className="text-[10px] uppercase tracking-wider text-[#999] mt-1.5 font-medium">AI Engine</div>
</div>
</div>
)}
{/* Quick Start */}
<QuickStart />
{/* Quick Actions — 1-col on mobile, 3-col on desktop */}
<div>
<p className="text-[10px] uppercase tracking-wider text-[#bbb] font-medium mb-3">Jump to</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{ACTIONS.map(({ id, Icon, label, sub }) => (
<button
key={id}
onClick={() => onNavigate?.(id)}
className="border border-[#eaeaea] rounded-lg p-4 bg-white hover:border-black hover:shadow-sm transition-all text-left group flex sm:block items-center gap-3"
>
<div className="flex items-start justify-between mb-0 sm:mb-3">
<div className="w-8 h-8 rounded-md border border-[#eaeaea] flex items-center justify-center group-hover:border-black transition-colors flex-shrink-0">
<Icon className="w-3.5 h-3.5 text-black" strokeWidth={1.5} />
</div>
<ArrowRight className="w-3.5 h-3.5 text-[#ccc] group-hover:text-black transition-colors mt-0.5 hidden sm:block" />
</div>
<div>
<div className="text-sm font-medium text-black">{label}</div>
<div className="text-xs text-[#999] mt-0.5 leading-relaxed">{sub}</div>
</div>
</button>
))}
</div>
</div>
</div>
);
}