Files
temetro/frontend/components/sidebar-02/nav-chat-history.tsx
T
Khalid Abdi 307cc3cc64 frontend: clinical UI fixes + chat/composer polish
- chat composer: attach works with text (sr-only file input); lighter
  bordered input box
- messages: defer "+" menu actions past close so file/appointment pickers fire
- tasks: top-level New task button
- chat: condensed appointment card with "View in calendar" deep-link;
  appointments page auto-opens the month calendar from ?calendar=&date=
- lab: work-queue rows expand to show task detail + complete
- inventory: clickable item detail dialog
- pharmacy: Dispense action records a dispense + "Recently dispensed" feed;
  expiring badge uses endDate and only flags <=2 days left
- prescriptions: keyboard nav in patient search, inventory-backed medication
  combobox (free-text fallback), optional start/end dates; thread dates through
- sidebar: whole nav scrolls as one
- i18n keys for all new strings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 20:45:30 +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 flex-col gap-0.5 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>
);
}