Files
temetro/frontend/components/sidebar-02/nav-chat-history.tsx
T
Khalid Abdi 3aa699aefe feat: clinic-wide AI (analytics/earnings/inventory + invoice-from-file),
real Live card, persistent chat history

Analytics & earnings:
- Analytics now carries real money computed from invoices (billed/paid/
  outstanding + by-month); new Earnings section on the Analysis page drawn with
  the project's Bklit chart components (shared EarningsChart).

AI agent reaches the whole clinic:
- new read tools getClinicInfo / getAnalytics / listInventory render clinic,
  analytics (with a Bklit earnings chart) and inventory cards in chat
- proposeInvoice turns an uploaded purchase/medication list into an invoice
  (new "invoice" action-preview kind → createInvoice); invoices/appointments
  auto-create/link a patient (ensurePatient) so they hit the Patients page

Live card:
- plots real data — patients checked in today — via GET /api/analytics/live
  (polled); value pill clamped and margins widened so nothing spills the card

Persistent AI chat history (Claude-style):
- ai_chat_threads + ai_chat_messages (migration 0016); per-user, org-scoped
  thread CRUD under /api/chat/threads
- chat panel owns a thread id, loads /?thread=<id>, and auto-saves after each
  exchange; sidebar lists past chats (open/delete), "New chat" starts fresh

Verified with backend typecheck + frontend tsc + next build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 22:25:15 +03:00

83 lines
2.7 KiB
TypeScript

"use client";
import { Trash2 } from "lucide-react";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { type MouseEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSidebar } from "@/components/ui/sidebar";
import {
deleteThread,
listThreads,
THREADS_CHANGED_EVENT,
type ThreadSummary,
} from "@/lib/ai-chat-history";
import { cn } from "@/lib/utils";
// Claude-style list of saved AI chats in the sidebar. Refreshes when a chat is
// saved/deleted (via the THREADS_CHANGED_EVENT). Hidden when collapsed or empty.
export function NavChatHistory() {
const { t } = useTranslation();
const { state } = useSidebar();
const pathname = usePathname();
const searchParams = useSearchParams();
const activeThread = searchParams.get("thread");
const [threads, setThreads] = useState<ThreadSummary[]>([]);
useEffect(() => {
const refresh = () => {
listThreads()
.then(setThreads)
.catch(() => {
/* not signed in / no clinic — just show nothing */
});
};
refresh();
window.addEventListener(THREADS_CHANGED_EVENT, refresh);
return () => window.removeEventListener(THREADS_CHANGED_EVENT, refresh);
}, []);
if (state === "collapsed" || threads.length === 0) return null;
const remove = async (event: MouseEvent, id: string) => {
event.preventDefault();
event.stopPropagation();
setThreads((prev) => prev.filter((x) => x.id !== id));
await deleteThread(id).catch(() => {
/* ignore */
});
};
return (
<div className="flex min-h-0 flex-col gap-0.5 overflow-y-auto px-2">
<span className="px-2 py-1 font-medium text-muted-foreground text-xs">
{t("chat.history.title")}
</span>
{threads.map((thread) => {
const active = pathname === "/" && activeThread === thread.id;
return (
<Link
className={cn(
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent",
active ? "bg-accent text-foreground" : "text-muted-foreground",
)}
href={`/?thread=${thread.id}`}
key={thread.id}
>
<span className="min-w-0 flex-1 truncate">{thread.title}</span>
<button
aria-label={t("chat.history.delete")}
className="shrink-0 opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
onClick={(event) => remove(event, thread.id)}
type="button"
>
<Trash2 className="size-3.5" />
</button>
</Link>
);
})}
</div>
);
}