mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 10:56:58 +00:00
3aa699aefe
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>
83 lines
2.7 KiB
TypeScript
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>
|
|
);
|
|
}
|