mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 10:56:58 +00:00
307cc3cc64
- 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>
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 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>
|
|
);
|
|
}
|