adds deleting

This commit is contained in:
2026-02-14 01:10:27 -08:00
parent 6f6dd434af
commit bec25aa943
4 changed files with 165 additions and 5 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Globe2, LogOut, MessageSquare, Plus, Search, SendHorizontal } from "lucide-preact";
import { Globe2, LogOut, MessageSquare, Plus, Search, SendHorizontal, Trash2 } from "lucide-preact";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -10,6 +10,8 @@ import { SearchResultsPanel } from "@/components/search/search-results-panel";
import {
createChat,
createSearch,
deleteChat,
deleteSearch,
getChat,
getSearch,
listChats,
@@ -33,6 +35,11 @@ type SidebarItem = SidebarSelection & {
updatedAt: string;
createdAt: string;
};
type ContextMenuState = {
item: SidebarSelection;
x: number;
y: number;
};
const PROVIDER_DEFAULT_MODELS: Record<Provider, string> = {
openai: "gpt-4.1-mini",
@@ -111,6 +118,8 @@ export default function App() {
const [model, setModel] = useState(PROVIDER_DEFAULT_MODELS.openai);
const [error, setError] = useState<string | null>(null);
const transcriptEndRef = useRef<HTMLDivElement>(null);
const contextMenuRef = useRef<HTMLDivElement>(null);
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const sidebarItems = useMemo(() => buildSidebarItems(chats, searches), [chats, searches]);
@@ -263,6 +272,7 @@ export default function App() {
const handleCreateChat = () => {
setError(null);
setContextMenu(null);
setDraftKind("chat");
setSelectedItem(null);
setSelectedChat(null);
@@ -271,12 +281,62 @@ export default function App() {
const handleCreateSearch = () => {
setError(null);
setContextMenu(null);
setDraftKind("search");
setSelectedItem(null);
setSelectedChat(null);
setSelectedSearch(null);
};
const openContextMenu = (event: MouseEvent, item: SidebarSelection) => {
event.preventDefault();
const menuWidth = 160;
const menuHeight = 40;
const padding = 8;
const x = Math.min(event.clientX, window.innerWidth - menuWidth - padding);
const y = Math.min(event.clientY, window.innerHeight - menuHeight - padding);
setContextMenu({ item, x: Math.max(padding, x), y: Math.max(padding, y) });
};
const handleDeleteFromContextMenu = async () => {
if (!contextMenu || isSending) return;
const target = contextMenu.item;
setContextMenu(null);
setError(null);
try {
if (target.kind === "chat") {
await deleteChat(target.id);
} else {
await deleteSearch(target.id);
}
await refreshCollections();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("bearer token")) {
handleAuthFailure(message);
} else {
setError(message);
}
}
};
useEffect(() => {
if (!contextMenu) return;
const handlePointerDown = (event: PointerEvent) => {
if (contextMenuRef.current?.contains(event.target as Node)) return;
setContextMenu(null);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setContextMenu(null);
};
window.addEventListener("pointerdown", handlePointerDown);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("pointerdown", handlePointerDown);
window.removeEventListener("keydown", handleKeyDown);
};
}, [contextMenu]);
const handleSendChat = async (content: string) => {
let chatId = draftKind === "chat" ? null : selectedItem?.kind === "chat" ? selectedItem.id : null;
@@ -446,6 +506,7 @@ export default function App() {
};
const handleLogout = () => {
setContextMenu(null);
logout();
resetWorkspaceState();
};
@@ -503,9 +564,11 @@ export default function App() {
active ? "bg-slate-700 text-slate-50" : "text-slate-200 hover:bg-slate-800"
)}
onClick={() => {
setContextMenu(null);
setDraftKind(null);
setSelectedItem({ kind: item.kind, id: item.id });
}}
onContextMenu={(event) => openContextMenu(event, { kind: item.kind, id: item.id })}
type="button"
>
<div className="flex items-center gap-2">
@@ -595,6 +658,24 @@ export default function App() {
</footer>
</main>
</div>
{contextMenu ? (
<div
ref={contextMenuRef}
className="fixed z-50 min-w-40 rounded-md border border-border bg-background p-1 shadow-md"
style={{ left: contextMenu.x, top: contextMenu.y }}
onContextMenu={(event) => event.preventDefault()}
>
<button
type="button"
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm text-red-600 transition hover:bg-muted disabled:text-muted-foreground"
onClick={() => void handleDeleteFromContextMenu()}
disabled={isSending}
>
<Trash2 className="h-3.5 w-3.5" />
Delete
</button>
</div>
) : null}
</div>
);
}

View File

@@ -96,7 +96,10 @@ export function setAuthToken(token: string | null) {
async function api<T>(path: string, init?: RequestInit): Promise<T> {
const headers = new Headers(init?.headers ?? {});
headers.set("Content-Type", "application/json");
const hasBody = init?.body !== undefined && init.body !== null;
if (hasBody && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
if (authToken) {
headers.set("Authorization", `Bearer ${authToken}`);
}
@@ -143,6 +146,10 @@ export async function getChat(chatId: string) {
return data.chat;
}
export async function deleteChat(chatId: string) {
await api<{ deleted: true }>(`/v1/chats/${chatId}`, { method: "DELETE" });
}
export async function listSearches() {
const data = await api<{ searches: SearchSummary[] }>("/v1/searches");
return data.searches;
@@ -161,6 +168,10 @@ export async function getSearch(searchId: string) {
return data.search;
}
export async function deleteSearch(searchId: string) {
await api<{ deleted: true }>(`/v1/searches/${searchId}`, { method: "DELETE" });
}
export async function runSearch(
searchId: string,
body: {