chore: ran linting

This commit is contained in:
Anish Sarkar 2026-03-07 04:46:48 +05:30
parent 37e1995546
commit f8b0e946ce
31 changed files with 768 additions and 754 deletions

View file

@ -23,15 +23,24 @@ SYNC_WINDOW_DAYS = 14
# Valid notification types - must match frontend InboxItemTypeEnum # Valid notification types - must match frontend InboxItemTypeEnum
NotificationType = Literal[ NotificationType = Literal[
"connector_indexing", "connector_deletion", "document_processing", "connector_indexing",
"new_mention", "comment_reply", "page_limit_exceeded", "connector_deletion",
"document_processing",
"new_mention",
"comment_reply",
"page_limit_exceeded",
] ]
# Category-to-types mapping for filtering by tab # Category-to-types mapping for filtering by tab
NotificationCategory = Literal["comments", "status"] NotificationCategory = Literal["comments", "status"]
CATEGORY_TYPES: dict[str, tuple[str, ...]] = { CATEGORY_TYPES: dict[str, tuple[str, ...]] = {
"comments": ("new_mention", "comment_reply"), "comments": ("new_mention", "comment_reply"),
"status": ("connector_indexing", "connector_deletion", "document_processing", "page_limit_exceeded"), "status": (
"connector_indexing",
"connector_deletion",
"document_processing",
"page_limit_exceeded",
),
} }
@ -152,7 +161,10 @@ async def get_notification_source_types(
document_result = await session.execute(document_query) document_result = await session.execute(document_query)
sources = [] sources = []
for source_type, category, count in [*connector_result.all(), *document_result.all()]: for source_type, category, count in [
*connector_result.all(),
*document_result.all(),
]:
if not source_type: if not source_type:
continue continue
sources.append( sources.append(
@ -300,24 +312,20 @@ async def list_notifications(
# Filter by source type (connector or document type from JSONB metadata) # Filter by source type (connector or document type from JSONB metadata)
if source_type: if source_type:
if source_type.startswith("connector:"): if source_type.startswith("connector:"):
connector_val = source_type[len("connector:"):] connector_val = source_type[len("connector:") :]
source_filter = ( source_filter = Notification.type.in_(
Notification.type.in_(("connector_indexing", "connector_deletion")) ("connector_indexing", "connector_deletion")
& ( ) & (
Notification.notification_metadata["connector_type"].astext Notification.notification_metadata["connector_type"].astext
== connector_val == connector_val
)
) )
query = query.where(source_filter) query = query.where(source_filter)
count_query = count_query.where(source_filter) count_query = count_query.where(source_filter)
elif source_type.startswith("doctype:"): elif source_type.startswith("doctype:"):
doctype_val = source_type[len("doctype:"):] doctype_val = source_type[len("doctype:") :]
source_filter = ( source_filter = Notification.type.in_(("document_processing",)) & (
Notification.type.in_(("document_processing",)) Notification.notification_metadata["document_type"].astext
& ( == doctype_val
Notification.notification_metadata["document_type"].astext
== doctype_val
)
) )
query = query.where(source_filter) query = query.where(source_filter)
count_query = count_query.where(source_filter) count_query = count_query.where(source_filter)
@ -328,11 +336,8 @@ async def list_notifications(
query = query.where(unread_filter) query = query.where(unread_filter)
count_query = count_query.where(unread_filter) count_query = count_query.where(unread_filter)
elif filter == "errors": elif filter == "errors":
error_filter = ( error_filter = (Notification.type == "page_limit_exceeded") | (
(Notification.type == "page_limit_exceeded") Notification.notification_metadata["status"].astext == "failed"
| (
Notification.notification_metadata["status"].astext == "failed"
)
) )
query = query.where(error_filter) query = query.where(error_filter)
count_query = count_query.where(error_filter) count_query = count_query.where(error_filter)

View file

@ -186,9 +186,7 @@ export function DashboardClientLayout({
return ( return (
<DocumentUploadDialogProvider> <DocumentUploadDialogProvider>
<OnboardingTour /> <OnboardingTour />
<LayoutDataProvider searchSpaceId={searchSpaceId}> <LayoutDataProvider searchSpaceId={searchSpaceId}>{children}</LayoutDataProvider>
{children}
</LayoutDataProvider>
</DocumentUploadDialogProvider> </DocumentUploadDialogProvider>
); );
} }

View file

@ -58,9 +58,7 @@ export function DocumentsFilters({
}, [typeCountsRecord]); }, [typeCountsRecord]);
return ( return (
<div <div className="flex select-none">
className="flex select-none"
>
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
{/* Type Filter */} {/* Type Filter */}
<Popover> <Popover>
@ -81,15 +79,15 @@ export function DocumentsFilters({
<PopoverContent className="w-64 !p-0 overflow-hidden" align="end"> <PopoverContent className="w-64 !p-0 overflow-hidden" align="end">
<div> <div>
{/* Search input */} {/* Search input */}
<div className="p-2 border-b border-neutral-700"> <div className="p-2 border-b border-neutral-700">
<div className="relative"> <div className="relative">
<Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-0.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input <Input
placeholder="Search types" placeholder="Search types"
value={typeSearchQuery} value={typeSearchQuery}
onChange={(e) => setTypeSearchQuery(e.target.value)} onChange={(e) => setTypeSearchQuery(e.target.value)}
className="h-6 pl-6 text-sm bg-transparent border-0 shadow-none focus-visible:ring-0" className="h-6 pl-6 text-sm bg-transparent border-0 shadow-none focus-visible:ring-0"
/> />
</div> </div>
</div> </div>
@ -139,11 +137,11 @@ export function DocumentsFilters({
)} )}
</div> </div>
{activeTypes.length > 0 && ( {activeTypes.length > 0 && (
<div className="px-3 pt-1.5 pb-1.5 border-t border-neutral-700"> <div className="px-3 pt-1.5 pb-1.5 border-t border-neutral-700">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground hover:bg-neutral-700" className="w-full h-7 text-[11px] text-muted-foreground hover:text-foreground hover:bg-neutral-700"
onClick={() => { onClick={() => {
activeTypes.forEach((t) => { activeTypes.forEach((t) => {
onToggleType(t, false); onToggleType(t, false);

View file

@ -452,9 +452,7 @@ export function DocumentsTableShell({
selectableDocs.length > 0 && selectableDocs.length > 0 &&
selectableDocs.every((d) => mentionedDocIds.has(d.id)); selectableDocs.every((d) => mentionedDocIds.has(d.id));
const someMentionedOnPage = const someMentionedOnPage =
hasChatMode && hasChatMode && selectableDocs.some((d) => mentionedDocIds.has(d.id)) && !allMentionedOnPage;
selectableDocs.some((d) => mentionedDocIds.has(d.id)) &&
!allMentionedOnPage;
const toggleAll = (checked: boolean) => { const toggleAll = (checked: boolean) => {
if (!onToggleChatMention) return; if (!onToggleChatMention) return;
@ -471,9 +469,7 @@ export function DocumentsTableShell({
const onSortHeader = (key: SortKey) => onSortChange(key); const onSortHeader = (key: SortKey) => onSortChange(key);
return ( return (
<div <div className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0">
className="bg-sidebar overflow-hidden select-none border-t border-border/50 flex-1 flex flex-col min-h-0"
>
{/* Desktop Table View */} {/* Desktop Table View */}
<div className="hidden md:flex md:flex-col flex-1 min-h-0"> <div className="hidden md:flex md:flex-col flex-1 min-h-0">
<Table className="table-fixed w-full"> <Table className="table-fixed w-full">
@ -548,91 +544,85 @@ export function DocumentsTableShell({
</div> </div>
) : sorted.length === 0 ? ( ) : sorted.length === 0 ? (
<div className="flex flex-1 w-full items-center justify-center"> <div className="flex flex-1 w-full items-center justify-center">
<div <div className="flex flex-col items-center gap-4 max-w-md px-4 text-center">
className="flex flex-col items-center gap-4 max-w-md px-4 text-center" <div className="rounded-full bg-muted/50 p-4">
> <FileX className="h-8 w-8 text-muted-foreground" />
<div className="rounded-full bg-muted/50 p-4"> </div>
<FileX className="h-8 w-8 text-muted-foreground" /> <div className="space-y-1.5">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</div> </div>
<div className="space-y-1.5">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</div> </div>
</div>
) : ( ) : (
<div ref={desktopScrollRef} className="flex-1 overflow-auto"> <div ref={desktopScrollRef} className="flex-1 overflow-auto">
<Table className="table-fixed w-full"> <Table className="table-fixed w-full">
<TableBody> <TableBody>
{sorted.map((doc) => { {sorted.map((doc) => {
const isMentioned = mentionedDocIds?.has(doc.id) ?? false; const isMentioned = mentionedDocIds?.has(doc.id) ?? false;
const canInteract = isSelectable(doc); const canInteract = isSelectable(doc);
const handleRowClick = () => { const handleRowClick = () => {
if (canInteract && onToggleChatMention) { if (canInteract && onToggleChatMention) {
onToggleChatMention(doc, isMentioned); onToggleChatMention(doc, isMentioned);
} }
}; };
return ( return (
<RowContextMenu <RowContextMenu
key={doc.id} key={doc.id}
doc={doc} doc={doc}
onPreview={handleViewDocument} onPreview={handleViewDocument}
onDelete={setDeleteDoc} onDelete={setDeleteDoc}
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
>
<tr
className={`border-b border-border/50 transition-colors ${
isMentioned
? "bg-primary/5 hover:bg-primary/8"
: "hover:bg-muted/30"
} ${canInteract && hasChatMode ? "cursor-pointer" : ""}`}
onClick={handleRowClick}
> >
<TableCell <tr
className="w-10 pl-3 pr-0 py-1.5 text-center" className={`border-b border-border/50 transition-colors ${
onClick={(e) => e.stopPropagation()} isMentioned ? "bg-primary/5 hover:bg-primary/8" : "hover:bg-muted/30"
} ${canInteract && hasChatMode ? "cursor-pointer" : ""}`}
onClick={handleRowClick}
> >
<div className="flex items-center justify-center h-full"> <TableCell
<Checkbox className="w-10 pl-3 pr-0 py-1.5 text-center"
checked={isMentioned} onClick={(e) => e.stopPropagation()}
onCheckedChange={() => handleRowClick()} >
disabled={!canInteract} <div className="flex items-center justify-center h-full">
aria-label={ <Checkbox
isMentioned ? "Remove from chat" : "Add to chat" checked={isMentioned}
} onCheckedChange={() => handleRowClick()}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`} disabled={!canInteract}
aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`}
/>
</div>
</TableCell>
<TableCell className="px-2 py-1.5 max-w-0">
<DocumentNameTooltip
doc={doc}
className="truncate block text-sm text-foreground cursor-default"
/> />
</div> </TableCell>
</TableCell> <TableCell className="w-10 px-0 py-1.5 text-center">
<TableCell className="px-2 py-1.5 max-w-0"> <Tooltip>
<DocumentNameTooltip <TooltipTrigger asChild>
doc={doc} <span className="flex items-center justify-center">
className="truncate block text-sm text-foreground cursor-default" {getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
/> </span>
</TableCell> </TooltipTrigger>
<TableCell className="w-10 px-0 py-1.5 text-center"> <TooltipContent side="top">
<Tooltip> {getDocumentTypeLabel(doc.document_type)}
<TooltipTrigger asChild> </TooltipContent>
<span className="flex items-center justify-center"> </Tooltip>
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")} </TableCell>
</span> <TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
</TooltipTrigger> <StatusIndicator status={doc.status} />
<TooltipContent side="top"> </TableCell>
{getDocumentTypeLabel(doc.document_type)} </tr>
</TooltipContent> </RowContextMenu>
</Tooltip>
</TableCell>
<TableCell className="w-12 pl-0 pr-3 py-1.5 text-center">
<StatusIndicator status={doc.status} />
</TableCell>
</tr>
</RowContextMenu>
); );
})} })}
</TableBody> </TableBody>
@ -669,9 +659,7 @@ export function DocumentsTableShell({
</div> </div>
) : sorted.length === 0 ? ( ) : sorted.length === 0 ? (
<div className="md:hidden flex flex-1 w-full items-center justify-center"> <div className="md:hidden flex flex-1 w-full items-center justify-center">
<div <div className="flex flex-col items-center gap-4 max-w-md px-4 text-center">
className="flex flex-col items-center gap-4 max-w-md px-4 text-center"
>
<div className="rounded-full bg-muted/50 p-4"> <div className="rounded-full bg-muted/50 p-4">
<FileX className="h-8 w-8 text-muted-foreground" /> <FileX className="h-8 w-8 text-muted-foreground" />
</div> </div>
@ -692,99 +680,94 @@ export function DocumentsTableShell({
ref={mobileScrollRef} ref={mobileScrollRef}
className="md:hidden divide-y divide-border/50 flex-1 overflow-auto" className="md:hidden divide-y divide-border/50 flex-1 overflow-auto"
> >
{sorted.map((doc) => { {sorted.map((doc) => {
const isMentioned = mentionedDocIds?.has(doc.id) ?? false; const isMentioned = mentionedDocIds?.has(doc.id) ?? false;
const canInteract = isSelectable(doc); const canInteract = isSelectable(doc);
const handleCardClick = () => { const handleCardClick = () => {
if (canInteract && onToggleChatMention) { if (canInteract && onToggleChatMention) {
onToggleChatMention(doc, isMentioned); onToggleChatMention(doc, isMentioned);
} }
}; };
return ( return (
<MobileCardWrapper <MobileCardWrapper key={doc.id} onLongPress={() => setMobileActionDoc(doc)}>
key={doc.id} <div
onLongPress={() => setMobileActionDoc(doc)} className={`relative px-3 py-2 transition-colors ${
> isMentioned ? "bg-primary/5" : "hover:bg-muted/20"
<div } ${canInteract && hasChatMode ? "cursor-pointer" : ""}`}
className={`relative px-3 py-2 transition-colors ${ >
isMentioned {canInteract && hasChatMode && (
? "bg-primary/5" <button
: "hover:bg-muted/20" type="button"
} ${canInteract && hasChatMode ? "cursor-pointer" : ""}`} className="absolute inset-0 z-0"
> aria-label={
{canInteract && hasChatMode && ( isMentioned ? `Remove ${doc.title} from chat` : `Add ${doc.title} to chat`
<button }
type="button" onClick={handleCardClick}
className="absolute inset-0 z-0" />
aria-label={isMentioned ? `Remove ${doc.title} from chat` : `Add ${doc.title} to chat`} )}
onClick={handleCardClick} <div className="relative z-10 flex items-center gap-3 pointer-events-none">
/> <span className="pointer-events-auto">
)} <Checkbox
<div className="relative z-10 flex items-center gap-3 pointer-events-none"> checked={isMentioned}
<span className="pointer-events-auto"> onCheckedChange={() => handleCardClick()}
<Checkbox disabled={!canInteract}
checked={isMentioned} aria-label={isMentioned ? "Remove from chat" : "Add to chat"}
onCheckedChange={() => handleCardClick()} className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`}
disabled={!canInteract} />
aria-label={isMentioned ? "Remove from chat" : "Add to chat"} </span>
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary shrink-0 ${!canInteract ? "opacity-40 cursor-not-allowed" : ""}`} <div className="flex-1 min-w-0">
/> <span className="truncate block text-sm text-foreground">{doc.title}</span>
</span> </div>
<div className="flex-1 min-w-0"> <div className="flex items-center gap-2 shrink-0">
<span className="truncate block text-sm text-foreground"> <span className="flex items-center justify-center">
{doc.title} {getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
</span> </span>
<StatusIndicator status={doc.status} />
</div>
</div>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> </MobileCardWrapper>
<span className="flex items-center justify-center">
{getDocumentTypeIcon(doc.document_type, "h-4 w-4")}
</span>
<StatusIndicator status={doc.status} />
</div>
</div>
</div>
</MobileCardWrapper>
); );
})} })}
{hasMore && <div ref={mobileSentinelRef} className="py-3" />} {hasMore && <div ref={mobileSentinelRef} className="py-3" />}
</div> </div>
)} )}
{/* Document Content Viewer */} {/* Document Content Viewer */}
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}> <Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
<DialogContent className="max-w-4xl max-w-[92%] md:max-w-4xl max-h-[75vh] md:max-h-[80vh] flex flex-col overflow-hidden pb-0 p-3 md:p-6 gap-2 md:gap-4"> <DialogContent className="max-w-4xl max-w-[92%] md:max-w-4xl max-h-[75vh] md:max-h-[80vh] flex flex-col overflow-hidden pb-0 p-3 md:p-6 gap-2 md:gap-4">
<DialogHeader className="flex-shrink-0"> <DialogHeader className="flex-shrink-0">
<DialogTitle className="text-sm md:text-lg leading-tight pr-6"> <DialogTitle className="text-sm md:text-lg leading-tight pr-6">
{viewingDoc?.title} {viewingDoc?.title}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div <div
onScroll={handlePreviewScroll} onScroll={handlePreviewScroll}
className={[ className={[
"overflow-y-auto flex-1 min-h-0 px-1 md:px-6 select-text", "overflow-y-auto flex-1 min-h-0 px-1 md:px-6 select-text",
"max-md:text-xs", "max-md:text-xs",
"max-md:[&_h1]:text-base! max-md:[&_h1]:mt-3!", "max-md:[&_h1]:text-base! max-md:[&_h1]:mt-3!",
"max-md:[&_h2]:text-sm! max-md:[&_h2]:mt-2!", "max-md:[&_h2]:text-sm! max-md:[&_h2]:mt-2!",
"max-md:[&_h3]:text-xs! max-md:[&_h3]:mt-2!", "max-md:[&_h3]:text-xs! max-md:[&_h3]:mt-2!",
"max-md:[&_h4]:text-xs!", "max-md:[&_h4]:text-xs!",
"max-md:[&_td]:text-[11px]! max-md:[&_td]:px-2! max-md:[&_td]:py-1.5!", "max-md:[&_td]:text-[11px]! max-md:[&_td]:px-2! max-md:[&_td]:py-1.5!",
"max-md:[&_th]:text-[11px]! max-md:[&_th]:px-2! max-md:[&_th]:py-1.5!", "max-md:[&_th]:text-[11px]! max-md:[&_th]:px-2! max-md:[&_th]:py-1.5!",
].join(" ")} ].join(" ")}
style={{ style={{
maskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`, maskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
WebkitMaskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`, WebkitMaskImage: `linear-gradient(to bottom, ${previewScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${previewScrollPos === "bottom" ? "black" : "transparent"})`,
}} }}
> >
{viewingLoading ? ( {viewingLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Spinner size="lg" className="text-muted-foreground" /> <Spinner size="lg" className="text-muted-foreground" />
</div> </div>
) : ( ) : (
<MarkdownViewer content={viewingContent} /> <MarkdownViewer content={viewingContent} />
)} )}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<AlertDialog open={!!deleteDoc} onOpenChange={(open) => !open && setDeleteDoc(null)}> <AlertDialog open={!!deleteDoc} onOpenChange={(open) => !open && setDeleteDoc(null)}>
@ -810,94 +793,86 @@ export function DocumentsTableShell({
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Mobile Document Actions Drawer */} {/* Mobile Document Actions Drawer */}
<Drawer open={!!mobileActionDoc} onOpenChange={(open) => !open && setMobileActionDoc(null)}> <Drawer open={!!mobileActionDoc} onOpenChange={(open) => !open && setMobileActionDoc(null)}>
<DrawerContent> <DrawerContent>
<DrawerHandle /> <DrawerHandle />
<DrawerHeader className="text-left"> <DrawerHeader className="text-left">
<DrawerTitle className="break-words text-base"> <DrawerTitle className="break-words text-base">{mobileActionDoc?.title}</DrawerTitle>
{mobileActionDoc?.title} <div className="space-y-0.5 text-xs mt-1">
</DrawerTitle> <p>
<div className="space-y-0.5 text-xs mt-1"> <span className="text-muted-foreground">Owner:</span>{" "}
<p> {mobileActionDoc?.created_by_name || mobileActionDoc?.created_by_email || "—"}
<span className="text-muted-foreground">Owner:</span>{" "} </p>
{mobileActionDoc?.created_by_name || <p>
mobileActionDoc?.created_by_email || <span className="text-muted-foreground">Created:</span>{" "}
"—"} {mobileActionDoc ? formatAbsoluteDate(mobileActionDoc.created_at) : ""}
</p> </p>
<p> </div>
<span className="text-muted-foreground">Created:</span>{" "} </DrawerHeader>
{mobileActionDoc <div className="px-4 pb-6 flex flex-col gap-2">
? formatAbsoluteDate(mobileActionDoc.created_at) <Button
: ""} variant="outline"
</p> className="justify-start gap-2"
onClick={() => {
if (mobileActionDoc) handleViewDocument(mobileActionDoc);
setMobileActionDoc(null);
}}
>
<Eye className="h-4 w-4" />
Preview
</Button>
{mobileActionDoc &&
EDITABLE_DOCUMENT_TYPES.includes(
mobileActionDoc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
) && (
<Button
variant="outline"
className="justify-start gap-2"
disabled={
mobileActionDoc.status?.state === "pending" ||
mobileActionDoc.status?.state === "processing" ||
(mobileActionDoc.document_type === "FILE" &&
mobileActionDoc.status?.state === "failed")
}
onClick={() => {
if (mobileActionDoc) {
router.push(`/dashboard/${searchSpaceId}/editor/${mobileActionDoc.id}`);
setMobileActionDoc(null);
}
}}
>
<PenLine className="h-4 w-4" />
Edit
</Button>
)}
{mobileActionDoc &&
!NON_DELETABLE_DOCUMENT_TYPES.includes(
mobileActionDoc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
) && (
<Button
variant="destructive"
className="justify-start gap-2"
disabled={
mobileActionDoc.status?.state === "pending" ||
mobileActionDoc.status?.state === "processing"
}
onClick={() => {
if (mobileActionDoc) {
setDeleteDoc(mobileActionDoc);
setMobileActionDoc(null);
}
}}
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
)}
</div> </div>
</DrawerHeader> </DrawerContent>
<div className="px-4 pb-6 flex flex-col gap-2"> </Drawer>
<Button </div>
variant="outline"
className="justify-start gap-2"
onClick={() => {
if (mobileActionDoc) handleViewDocument(mobileActionDoc);
setMobileActionDoc(null);
}}
>
<Eye className="h-4 w-4" />
Preview
</Button>
{mobileActionDoc &&
EDITABLE_DOCUMENT_TYPES.includes(
mobileActionDoc.document_type as (typeof EDITABLE_DOCUMENT_TYPES)[number]
) && (
<Button
variant="outline"
className="justify-start gap-2"
disabled={
mobileActionDoc.status?.state === "pending" ||
mobileActionDoc.status?.state === "processing" ||
(mobileActionDoc.document_type === "FILE" &&
mobileActionDoc.status?.state === "failed")
}
onClick={() => {
if (mobileActionDoc) {
router.push(
`/dashboard/${searchSpaceId}/editor/${mobileActionDoc.id}`
);
setMobileActionDoc(null);
}
}}
>
<PenLine className="h-4 w-4" />
Edit
</Button>
)}
{mobileActionDoc &&
!NON_DELETABLE_DOCUMENT_TYPES.includes(
mobileActionDoc.document_type as (typeof NON_DELETABLE_DOCUMENT_TYPES)[number]
) && (
<Button
variant="destructive"
className="justify-start gap-2"
disabled={
mobileActionDoc.status?.state === "pending" ||
mobileActionDoc.status?.state === "processing"
}
onClick={() => {
if (mobileActionDoc) {
setDeleteDoc(mobileActionDoc);
setMobileActionDoc(null);
}
}}
>
<Trash2 className="h-4 w-4" />
Delete
</Button>
)}
</div>
</DrawerContent>
</Drawer>
</div>
); );
} }

View file

@ -122,9 +122,7 @@ export function RowActions({
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)} onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
disabled={isDeleteDisabled} disabled={isDeleteDisabled}
className={ className={
isDeleteDisabled isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
? "text-muted-foreground cursor-not-allowed opacity-50"
: ""
} }
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
@ -175,9 +173,7 @@ export function RowActions({
onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)} onClick={() => !isDeleteDisabled && setIsDeleteOpen(true)}
disabled={isDeleteDisabled} disabled={isDeleteDisabled}
className={ className={
isDeleteDisabled isDeleteDisabled ? "text-muted-foreground cursor-not-allowed opacity-50" : ""
? "text-muted-foreground cursor-not-allowed opacity-50"
: ""
} }
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />

View file

@ -1670,9 +1670,7 @@ export default function NewChatPage() {
{/* <WriteTodosToolUI /> Disabled for now */} {/* <WriteTodosToolUI /> Disabled for now */}
<div className="flex h-[calc(100dvh-64px)] overflow-hidden"> <div className="flex h-[calc(100dvh-64px)] overflow-hidden">
<div className="flex-1 flex flex-col min-w-0 overflow-hidden"> <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
<Thread <Thread messageThinkingSteps={messageThinkingSteps} />
messageThinkingSteps={messageThinkingSteps}
/>
</div> </div>
<ReportPanel /> <ReportPanel />
</div> </div>

View file

@ -13,7 +13,9 @@ export const mentionedDocumentsAtom = atom<Pick<Document, "id" | "title" | "docu
* Atom to store documents selected via the sidebar checkboxes / row clicks. * Atom to store documents selected via the sidebar checkboxes / row clicks.
* These are NOT inserted as chips the composer shows a count badge instead. * These are NOT inserted as chips the composer shows a count badge instead.
*/ */
export const sidebarSelectedDocumentsAtom = atom<Pick<Document, "id" | "title" | "document_type">[]>([]); export const sidebarSelectedDocumentsAtom = atom<
Pick<Document, "id" | "title" | "document_type">[]
>([]);
/** /**
* Derived read-only atom that merges @-mention chips and sidebar selections * Derived read-only atom that merges @-mention chips and sidebar selections

View file

@ -1,14 +1,6 @@
"use client"; "use client";
import { import { Bell, ExternalLink, Info, type LucideIcon, Rocket, Wrench, Zap } from "lucide-react";
Bell,
ExternalLink,
Info,
type LucideIcon,
Rocket,
Wrench,
Zap,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -114,4 +106,3 @@ export function AnnouncementCard({ announcement }: { announcement: AnnouncementW
</Card> </Card>
); );
} }

View file

@ -15,4 +15,3 @@ export function AnnouncementsEmptyState() {
</div> </div>
); );
} }

View file

@ -170,83 +170,83 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
// Create a chip element for a document // Create a chip element for a document
const createChipElement = useCallback( const createChipElement = useCallback(
(doc: MentionedDocument): HTMLSpanElement => { (doc: MentionedDocument): HTMLSpanElement => {
const chip = document.createElement("span"); const chip = document.createElement("span");
chip.setAttribute(CHIP_DATA_ATTR, "true"); chip.setAttribute(CHIP_DATA_ATTR, "true");
chip.setAttribute(CHIP_ID_ATTR, String(doc.id)); chip.setAttribute(CHIP_ID_ATTR, String(doc.id));
chip.setAttribute(CHIP_DOCTYPE_ATTR, doc.document_type ?? "UNKNOWN"); chip.setAttribute(CHIP_DOCTYPE_ATTR, doc.document_type ?? "UNKNOWN");
chip.contentEditable = "false"; chip.contentEditable = "false";
chip.className = chip.className =
"inline-flex items-center gap-1 mx-0.5 px-1 py-0.5 rounded bg-primary/10 text-xs font-bold text-primary/60 select-none cursor-default"; "inline-flex items-center gap-1 mx-0.5 px-1 py-0.5 rounded bg-primary/10 text-xs font-bold text-primary/60 select-none cursor-default";
chip.style.userSelect = "none"; chip.style.userSelect = "none";
chip.style.verticalAlign = "baseline"; chip.style.verticalAlign = "baseline";
// Container that swaps between icon and remove button on hover // Container that swaps between icon and remove button on hover
const iconContainer = document.createElement("span"); const iconContainer = document.createElement("span");
iconContainer.className = "shrink-0 flex items-center size-3 relative"; iconContainer.className = "shrink-0 flex items-center size-3 relative";
const iconSpan = document.createElement("span"); const iconSpan = document.createElement("span");
iconSpan.className = "flex items-center text-muted-foreground"; iconSpan.className = "flex items-center text-muted-foreground";
iconSpan.innerHTML = ReactDOMServer.renderToString( iconSpan.innerHTML = ReactDOMServer.renderToString(
getConnectorIcon(doc.document_type ?? "UNKNOWN", "h-3 w-3") getConnectorIcon(doc.document_type ?? "UNKNOWN", "h-3 w-3")
); );
const removeBtn = document.createElement("button"); const removeBtn = document.createElement("button");
removeBtn.type = "button"; removeBtn.type = "button";
removeBtn.className = removeBtn.className =
"size-3 items-center justify-center rounded-full text-muted-foreground transition-colors"; "size-3 items-center justify-center rounded-full text-muted-foreground transition-colors";
removeBtn.style.display = "none"; removeBtn.style.display = "none";
removeBtn.innerHTML = ReactDOMServer.renderToString( removeBtn.innerHTML = ReactDOMServer.renderToString(
createElement(X, { className: "h-3 w-3", strokeWidth: 2.5 }) createElement(X, { className: "h-3 w-3", strokeWidth: 2.5 })
); );
removeBtn.onclick = (e) => { removeBtn.onclick = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
chip.remove(); chip.remove();
const docKey = `${doc.document_type ?? "UNKNOWN"}:${doc.id}`; const docKey = `${doc.document_type ?? "UNKNOWN"}:${doc.id}`;
setMentionedDocs((prev) => { setMentionedDocs((prev) => {
const next = new Map(prev); const next = new Map(prev);
next.delete(docKey); next.delete(docKey);
return next; return next;
}); });
onDocumentRemove?.(doc.id, doc.document_type); onDocumentRemove?.(doc.id, doc.document_type);
focusAtEnd(); focusAtEnd();
}; };
const titleSpan = document.createElement("span"); const titleSpan = document.createElement("span");
titleSpan.className = "max-w-[120px] truncate"; titleSpan.className = "max-w-[120px] truncate";
titleSpan.textContent = doc.title; titleSpan.textContent = doc.title;
titleSpan.title = doc.title; titleSpan.title = doc.title;
titleSpan.setAttribute("data-mention-title", "true"); titleSpan.setAttribute("data-mention-title", "true");
const statusSpan = document.createElement("span"); const statusSpan = document.createElement("span");
statusSpan.setAttribute(CHIP_STATUS_ATTR, "true"); statusSpan.setAttribute(CHIP_STATUS_ATTR, "true");
statusSpan.className = "text-[10px] font-semibold opacity-80 hidden"; statusSpan.className = "text-[10px] font-semibold opacity-80 hidden";
const isTouchDevice = window.matchMedia("(hover: none)").matches; const isTouchDevice = window.matchMedia("(hover: none)").matches;
if (isTouchDevice) { if (isTouchDevice) {
// Mobile: icon on left, title, X on right // Mobile: icon on left, title, X on right
chip.appendChild(iconSpan); chip.appendChild(iconSpan);
chip.appendChild(titleSpan); chip.appendChild(titleSpan);
chip.appendChild(statusSpan); chip.appendChild(statusSpan);
removeBtn.style.display = "flex";
removeBtn.className += " ml-0.5";
chip.appendChild(removeBtn);
} else {
// Desktop: icon/X swap on hover in the same slot
iconContainer.appendChild(iconSpan);
iconContainer.appendChild(removeBtn);
chip.addEventListener("mouseenter", () => {
iconSpan.style.display = "none";
removeBtn.style.display = "flex"; removeBtn.style.display = "flex";
}); removeBtn.className += " ml-0.5";
chip.addEventListener("mouseleave", () => { chip.appendChild(removeBtn);
iconSpan.style.display = ""; } else {
removeBtn.style.display = "none"; // Desktop: icon/X swap on hover in the same slot
}); iconContainer.appendChild(iconSpan);
chip.appendChild(iconContainer); iconContainer.appendChild(removeBtn);
chip.appendChild(titleSpan); chip.addEventListener("mouseenter", () => {
chip.appendChild(statusSpan); iconSpan.style.display = "none";
} removeBtn.style.display = "flex";
});
chip.addEventListener("mouseleave", () => {
iconSpan.style.display = "";
removeBtn.style.display = "none";
});
chip.appendChild(iconContainer);
chip.appendChild(titleSpan);
chip.appendChild(statusSpan);
}
return chip; return chip;
}, },

View file

@ -461,9 +461,7 @@ const Composer: FC = () => {
/>, />,
document.body document.body
)} )}
<ComposerAction <ComposerAction isBlockedByOtherUser={isBlockedByOtherUser} />
isBlockedByOtherUser={isBlockedByOtherUser}
/>
</div> </div>
</ComposerPrimitive.Root> </ComposerPrimitive.Root>
); );
@ -473,9 +471,7 @@ interface ComposerActionProps {
isBlockedByOtherUser?: boolean; isBlockedByOtherUser?: boolean;
} }
const ComposerAction: FC<ComposerActionProps> = ({ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false }) => {
isBlockedByOtherUser = false,
}) => {
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom); const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
const sidebarDocs = useAtomValue(sidebarSelectedDocumentsAtom); const sidebarDocs = useAtomValue(sidebarSelectedDocumentsAtom);
const setDocumentsSidebarOpen = useSetAtom(documentsSidebarOpenAtom); const setDocumentsSidebarOpen = useSetAtom(documentsSidebarOpenAtom);
@ -502,10 +498,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
return userConfigs?.some((c) => c.id === agentLlmId) ?? false; return userConfigs?.some((c) => c.id === agentLlmId) ?? false;
}, [preferences, globalConfigs, userConfigs]); }, [preferences, globalConfigs, userConfigs]);
const isSendDisabled = const isSendDisabled = isComposerEmpty || !hasModelConfigured || isBlockedByOtherUser;
isComposerEmpty ||
!hasModelConfigured ||
isBlockedByOtherUser;
return ( return (
<div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between"> <div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
@ -542,47 +535,47 @@ const ComposerAction: FC<ComposerActionProps> = ({
</button> </button>
)} )}
<AssistantIf condition={({ thread }) => !thread.isRunning}> <AssistantIf condition={({ thread }) => !thread.isRunning}>
<ComposerPrimitive.Send asChild disabled={isSendDisabled}> <ComposerPrimitive.Send asChild disabled={isSendDisabled}>
<TooltipIconButton <TooltipIconButton
tooltip={ tooltip={
isBlockedByOtherUser isBlockedByOtherUser
? "Wait for AI to finish responding" ? "Wait for AI to finish responding"
: !hasModelConfigured : !hasModelConfigured
? "Please select a model from the header to start chatting" ? "Please select a model from the header to start chatting"
: isComposerEmpty : isComposerEmpty
? "Enter a message to send" ? "Enter a message to send"
: "Send message" : "Send message"
} }
side="bottom" side="bottom"
type="submit" type="submit"
variant="default" variant="default"
size="icon" size="icon"
className={cn( className={cn(
"aui-composer-send size-8 rounded-full", "aui-composer-send size-8 rounded-full",
isSendDisabled && "cursor-not-allowed opacity-50" isSendDisabled && "cursor-not-allowed opacity-50"
)} )}
aria-label="Send message" aria-label="Send message"
disabled={isSendDisabled} disabled={isSendDisabled}
> >
<ArrowUpIcon className="aui-composer-send-icon size-4" /> <ArrowUpIcon className="aui-composer-send-icon size-4" />
</TooltipIconButton> </TooltipIconButton>
</ComposerPrimitive.Send> </ComposerPrimitive.Send>
</AssistantIf> </AssistantIf>
<AssistantIf condition={({ thread }) => thread.isRunning}> <AssistantIf condition={({ thread }) => thread.isRunning}>
<ComposerPrimitive.Cancel asChild> <ComposerPrimitive.Cancel asChild>
<Button <Button
type="button" type="button"
variant="default" variant="default"
size="icon" size="icon"
className="aui-composer-cancel size-8 rounded-full" className="aui-composer-cancel size-8 rounded-full"
aria-label="Stop generating" aria-label="Stop generating"
> >
<SquareIcon className="aui-composer-cancel-icon size-3 fill-current" /> <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
</Button> </Button>
</ComposerPrimitive.Cancel> </ComposerPrimitive.Cancel>
</AssistantIf> </AssistantIf>
</div> </div>
</div> </div>
); );

View file

@ -2,12 +2,7 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { import { AlertTriangle, Inbox, Megaphone, SquareLibrary } from "lucide-react";
AlertTriangle,
Inbox,
Megaphone,
SquareLibrary,
} from "lucide-react";
import { useParams, usePathname, useRouter } from "next/navigation"; import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
@ -68,10 +63,7 @@ function formatInboxCount(count: number): string {
return `${thousands}k+`; return `${thousands}k+`;
} }
export function LayoutDataProvider({ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProviderProps) {
searchSpaceId,
children,
}: LayoutDataProviderProps) {
const t = useTranslations("dashboard"); const t = useTranslations("dashboard");
const tCommon = useTranslations("common"); const tCommon = useTranslations("common");
const tSidebar = useTranslations("sidebar"); const tSidebar = useTranslations("sidebar");
@ -186,7 +178,6 @@ export function LayoutDataProvider({
} }
}, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, router]); }, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, router]);
// Delete dialogs state // Delete dialogs state
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false); const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null); const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
@ -683,9 +674,7 @@ export function LayoutDataProvider({
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel disabled={isDeletingChat}> <AlertDialogCancel disabled={isDeletingChat}>{tCommon("cancel")}</AlertDialogCancel>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();

View file

@ -160,9 +160,9 @@ export function LayoutShell({
<SidebarProvider value={sidebarContextValue}> <SidebarProvider value={sidebarContextValue}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<div className={cn("flex h-screen w-full flex-col bg-background", className)}> <div className={cn("flex h-screen w-full flex-col bg-background", className)}>
<Header <Header
mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />} mobileMenuTrigger={<MobileSidebarTrigger onClick={() => setMobileMenuOpen(true)} />}
/> />
<MobileSidebar <MobileSidebar
isOpen={mobileMenuOpen} isOpen={mobileMenuOpen}

View file

@ -364,72 +364,72 @@ export function AllPrivateChatsSidebar({
isBusy && "opacity-50 pointer-events-none" isBusy && "opacity-50 pointer-events-none"
)} )}
> >
{isMobile ? ( {isMobile ? (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (wasLongPress()) return; if (wasLongPress()) return;
handleThreadClick(thread.id); handleThreadClick(thread.id);
}} }}
onTouchStart={() => { onTouchStart={() => {
pendingThreadIdRef.current = thread.id; pendingThreadIdRef.current = thread.id;
longPressHandlers.onTouchStart(); longPressHandlers.onTouchStart();
}} }}
onTouchEnd={longPressHandlers.onTouchEnd} onTouchEnd={longPressHandlers.onTouchEnd}
onTouchMove={longPressHandlers.onTouchMove} onTouchMove={longPressHandlers.onTouchMove}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
) : (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
</p>
</TooltipContent>
</Tooltip>
)}
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
isMobile
? "opacity-0 pointer-events-none absolute"
: "md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isBusy} disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
> >
{isDeleting ? ( <MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<Spinner size="xs" /> <span className="truncate">{thread.title || "New Chat"}</span>
) : ( </button>
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" /> ) : (
)} <Tooltip>
<span className="sr-only">{t("more_options") || "More options"}</span> <TooltipTrigger asChild>
</Button> <button
</DropdownMenuTrigger> type="button"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
</p>
</TooltipContent>
</Tooltip>
)}
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
isMobile
? "opacity-0 pointer-events-none absolute"
: "md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isBusy}
>
{isDeleting ? (
<Spinner size="xs" />
) : (
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
)}
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80"> <DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && ( {!thread.archived && (
<DropdownMenuItem <DropdownMenuItem
@ -456,9 +456,7 @@ export function AllPrivateChatsSidebar({
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem onClick={() => handleDeleteThread(thread.id)}>
onClick={() => handleDeleteThread(thread.id)}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete") || "Delete"}</span> <span>{t("delete") || "Delete"}</span>
</DropdownMenuItem> </DropdownMenuItem>

View file

@ -364,72 +364,72 @@ export function AllSharedChatsSidebar({
isBusy && "opacity-50 pointer-events-none" isBusy && "opacity-50 pointer-events-none"
)} )}
> >
{isMobile ? ( {isMobile ? (
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
if (wasLongPress()) return; if (wasLongPress()) return;
handleThreadClick(thread.id); handleThreadClick(thread.id);
}} }}
onTouchStart={() => { onTouchStart={() => {
pendingThreadIdRef.current = thread.id; pendingThreadIdRef.current = thread.id;
longPressHandlers.onTouchStart(); longPressHandlers.onTouchStart();
}} }}
onTouchEnd={longPressHandlers.onTouchEnd} onTouchEnd={longPressHandlers.onTouchEnd}
onTouchMove={longPressHandlers.onTouchMove} onTouchMove={longPressHandlers.onTouchMove}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
) : (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
</p>
</TooltipContent>
</Tooltip>
)}
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
isMobile
? "opacity-0 pointer-events-none absolute"
: "md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isBusy} disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
> >
{isDeleting ? ( <MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<Spinner size="xs" /> <span className="truncate">{thread.title || "New Chat"}</span>
) : ( </button>
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" /> ) : (
)} <Tooltip>
<span className="sr-only">{t("more_options") || "More options"}</span> <TooltipTrigger asChild>
</Button> <button
</DropdownMenuTrigger> type="button"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<MessageCircleMore className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{thread.title || "New Chat"}</span>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}
</p>
</TooltipContent>
</Tooltip>
)}
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
isMobile
? "opacity-0 pointer-events-none absolute"
: "md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
"transition-opacity"
)}
disabled={isBusy}
>
{isDeleting ? (
<Spinner size="xs" />
) : (
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
)}
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80"> <DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && ( {!thread.archived && (
<DropdownMenuItem <DropdownMenuItem
@ -456,9 +456,7 @@ export function AllSharedChatsSidebar({
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem onClick={() => handleDeleteThread(thread.id)}>
onClick={() => handleDeleteThread(thread.id)}
>
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete") || "Delete"}</span> <span>{t("delete") || "Delete"}</span>
</DropdownMenuItem> </DropdownMenuItem>

View file

@ -72,4 +72,3 @@ export function AnnouncementsSidebar({
</SidebarSlideOutPanel> </SidebarSlideOutPanel>
); );
} }

View file

@ -73,12 +73,12 @@ export function ChatListItem({
</button> </button>
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */} {/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}
<div className={cn( <div
"absolute right-1 top-1/2 -translate-y-1/2 transition-opacity", className={cn(
isMobile "absolute right-1 top-1/2 -translate-y-1/2 transition-opacity",
? "opacity-0 pointer-events-none" isMobile ? "opacity-0 pointer-events-none" : "opacity-0 group-hover/item:opacity-100"
: "opacity-0 group-hover/item:opacity-100" )}
)}> >
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6"> <Button variant="ghost" size="icon" className="h-6 w-6">
@ -125,7 +125,7 @@ export function ChatListItem({
e.stopPropagation(); e.stopPropagation();
onDelete(); onDelete();
}} }}
> >
<Trash2 className="mr-2 h-4 w-4" /> <Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete")}</span> <span>{t("delete")}</span>
</DropdownMenuItem> </DropdownMenuItem>

View file

@ -41,10 +41,7 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom); const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom); const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom);
const mentionedDocIds = useMemo( const mentionedDocIds = useMemo(() => new Set(sidebarDocs.map((d) => d.id)), [sidebarDocs]);
() => new Set(sidebarDocs.map((d) => d.id)),
[sidebarDocs]
);
const handleToggleChatMention = useCallback( const handleToggleChatMention = useCallback(
(doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => { (doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => {
@ -53,7 +50,10 @@ export function DocumentsSidebar({ open, onOpenChange }: DocumentsSidebarProps)
} else { } else {
setSidebarDocs((prev) => { setSidebarDocs((prev) => {
if (prev.some((d) => d.id === doc.id)) return prev; if (prev.some((d) => d.id === doc.id)) return prev;
return [...prev, { id: doc.id, title: doc.title, document_type: doc.document_type as DocumentTypeEnum }]; return [
...prev,
{ id: doc.id, title: doc.title, document_type: doc.document_type as DocumentTypeEnum },
];
}); });
} }
}, },

View file

@ -638,58 +638,56 @@ export function InboxSidebar({
)} )}
</div> </div>
</div> </div>
{activeTab === "status" && statusSourceOptions.length > 0 && ( {activeTab === "status" && statusSourceOptions.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs text-muted-foreground/80 font-medium px-1"> <p className="text-xs text-muted-foreground/80 font-medium px-1">
{t("sources") || "Sources"} {t("sources") || "Sources"}
</p> </p>
<div className="space-y-1"> <div className="space-y-1">
<button
type="button"
onClick={() => {
setSelectedSource(null);
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
selectedSource === null
? "bg-primary/10 text-primary"
: "hover:bg-muted"
)}
>
<span className="flex items-center gap-2">
<LayoutGrid className="h-4 w-4" />
<span>{t("all_sources") || "All sources"}</span>
</span>
{selectedSource === null && <Check className="h-4 w-4" />}
</button>
{statusSourceOptions.map((source) => (
<button <button
key={source.key}
type="button" type="button"
onClick={() => { onClick={() => {
setSelectedSource(source.key); setSelectedSource(null);
setFilterDrawerOpen(false); setFilterDrawerOpen(false);
}} }}
className={cn( className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors", "flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
selectedSource === source.key selectedSource === null
? "bg-primary/10 text-primary" ? "bg-primary/10 text-primary"
: "hover:bg-muted" : "hover:bg-muted"
)} )}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{getConnectorIcon(source.type, "h-4 w-4")} <LayoutGrid className="h-4 w-4" />
<span>{source.displayName}</span> <span>{t("all_sources") || "All sources"}</span>
</span> </span>
{selectedSource === source.key && ( {selectedSource === null && <Check className="h-4 w-4" />}
<Check className="h-4 w-4" />
)}
</button> </button>
))} {statusSourceOptions.map((source) => (
<button
key={source.key}
type="button"
onClick={() => {
setSelectedSource(source.key);
setFilterDrawerOpen(false);
}}
className={cn(
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors",
selectedSource === source.key
? "bg-primary/10 text-primary"
: "hover:bg-muted"
)}
>
<span className="flex items-center gap-2">
{getConnectorIcon(source.type, "h-4 w-4")}
<span>{source.displayName}</span>
</span>
{selectedSource === source.key && <Check className="h-4 w-4" />}
</button>
))}
</div>
</div> </div>
</div> )}
)}
</div> </div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@ -712,7 +710,10 @@ export function InboxSidebar({
</Tooltip> </Tooltip>
<DropdownMenuContent <DropdownMenuContent
align="end" align="end"
className={cn("z-80 select-none max-h-[60vh] overflow-hidden flex flex-col", activeTab === "status" ? "w-52" : "w-44")} className={cn(
"z-80 select-none max-h-[60vh] overflow-hidden flex flex-col",
activeTab === "status" ? "w-52" : "w-44"
)}
> >
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal"> <DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal">
{t("filter") || "Filter"} {t("filter") || "Filter"}
@ -749,45 +750,45 @@ export function InboxSidebar({
{activeFilter === "errors" && <Check className="h-4 w-4" />} {activeFilter === "errors" && <Check className="h-4 w-4" />}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
{activeTab === "status" && statusSourceOptions.length > 0 && ( {activeTab === "status" && statusSourceOptions.length > 0 && (
<> <>
<DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal mt-2"> <DropdownMenuLabel className="text-xs text-muted-foreground/80 font-normal mt-2">
{t("sources") || "Sources"} {t("sources") || "Sources"}
</DropdownMenuLabel> </DropdownMenuLabel>
<div <div
className="relative max-h-[30vh] overflow-y-auto overflow-x-hidden -mb-1" className="relative max-h-[30vh] overflow-y-auto overflow-x-hidden -mb-1"
onScroll={handleConnectorScroll} onScroll={handleConnectorScroll}
style={{ style={{
maskImage: `linear-gradient(to bottom, ${connectorScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${connectorScrollPos === "bottom" ? "black" : "transparent"})`, maskImage: `linear-gradient(to bottom, ${connectorScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${connectorScrollPos === "bottom" ? "black" : "transparent"})`,
WebkitMaskImage: `linear-gradient(to bottom, ${connectorScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${connectorScrollPos === "bottom" ? "black" : "transparent"})`, WebkitMaskImage: `linear-gradient(to bottom, ${connectorScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${connectorScrollPos === "bottom" ? "black" : "transparent"})`,
}} }}
>
<DropdownMenuItem
onClick={() => setSelectedSource(null)}
className="flex items-center justify-between"
> >
<span className="flex items-center gap-2">
<LayoutGrid className="h-4 w-4" />
<span>{t("all_sources") || "All sources"}</span>
</span>
{selectedSource === null && <Check className="h-4 w-4" />}
</DropdownMenuItem>
{statusSourceOptions.map((source) => (
<DropdownMenuItem <DropdownMenuItem
key={source.key} onClick={() => setSelectedSource(null)}
onClick={() => setSelectedSource(source.key)}
className="flex items-center justify-between" className="flex items-center justify-between"
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
{getConnectorIcon(source.type, "h-4 w-4")} <LayoutGrid className="h-4 w-4" />
<span>{source.displayName}</span> <span>{t("all_sources") || "All sources"}</span>
</span> </span>
{selectedSource === source.key && <Check className="h-4 w-4" />} {selectedSource === null && <Check className="h-4 w-4" />}
</DropdownMenuItem> </DropdownMenuItem>
))} {statusSourceOptions.map((source) => (
</div> <DropdownMenuItem
</> key={source.key}
)} onClick={() => setSelectedSource(source.key)}
className="flex items-center justify-between"
>
<span className="flex items-center gap-2">
{getConnectorIcon(source.type, "h-4 w-4")}
<span>{source.displayName}</span>
</span>
{selectedSource === source.key && <Check className="h-4 w-4" />}
</DropdownMenuItem>
))}
</div>
</>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}

View file

@ -215,7 +215,12 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
</div> </div>
<div className="flex-1 text-left min-w-0"> <div className="flex-1 text-left min-w-0">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className={cn("text-sm font-medium", isSelected && "text-primary dark:text-white")}> <span
className={cn(
"text-sm font-medium",
isSelected && "text-primary dark:text-white"
)}
>
{option.label} {option.label}
</span> </span>
</div> </div>

View file

@ -143,11 +143,11 @@ export function ImageConfigDialog({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
if (mode === "create") { if (mode === "create") {
const result = await createConfig({ const result = await createConfig({
name: formData.name, name: formData.name,
provider: formData.provider as ImageGenProvider, provider: formData.provider as ImageGenProvider,
model_name: formData.model_name, model_name: formData.model_name,
api_key: formData.api_key, api_key: formData.api_key,
api_base: formData.api_base || undefined, api_base: formData.api_base || undefined,
api_version: formData.api_version || undefined, api_version: formData.api_version || undefined,
description: formData.description || undefined, description: formData.description || undefined,
@ -162,14 +162,14 @@ export function ImageConfigDialog({
toast.success("Image model created and assigned!"); toast.success("Image model created and assigned!");
onOpenChange(false); onOpenChange(false);
} else if (!isGlobal && config) { } else if (!isGlobal && config) {
await updateConfig({ await updateConfig({
id: config.id, id: config.id,
data: { data: {
name: formData.name, name: formData.name,
description: formData.description || undefined, description: formData.description || undefined,
provider: formData.provider as ImageGenProvider, provider: formData.provider as ImageGenProvider,
model_name: formData.model_name, model_name: formData.model_name,
api_key: formData.api_key, api_key: formData.api_key,
api_base: formData.api_base || undefined, api_base: formData.api_base || undefined,
api_version: formData.api_version || undefined, api_version: formData.api_version || undefined,
}, },
@ -248,7 +248,9 @@ export function ImageConfigDialog({
"flex flex-col overflow-hidden" "flex flex-col overflow-hidden"
)} )}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => { if (e.key === "Escape") onOpenChange(false); }} onKeyDown={(e) => {
if (e.key === "Escape") onOpenChange(false);
}}
> >
{/* Header */} {/* Header */}
<div className="flex items-start justify-between px-6 pt-6 pb-4"> <div className="flex items-start justify-between px-6 pt-6 pb-4">
@ -268,7 +270,9 @@ export function ImageConfigDialog({
</div> </div>
<p className="text-sm text-muted-foreground">{getSubtitle()}</p> <p className="text-sm text-muted-foreground">{getSubtitle()}</p>
{config && !isAutoMode && mode !== "create" && ( {config && !isAutoMode && mode !== "create" && (
<p className="text-xs font-mono text-muted-foreground/70">{config.model_name}</p> <p className="text-xs font-mono text-muted-foreground/70">
{config.model_name}
</p>
)} )}
</div> </div>
<Button <Button
@ -404,7 +408,10 @@ export function ImageConfigDialog({
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0 bg-muted dark:border-neutral-700" align="start"> <PopoverContent
className="w-full p-0 bg-muted dark:border-neutral-700"
align="start"
>
<Command className="bg-transparent"> <Command className="bg-transparent">
<CommandInput <CommandInput
placeholder="Search or type model..." placeholder="Search or type model..."
@ -505,7 +512,7 @@ export function ImageConfigDialog({
> >
Cancel Cancel
</Button> </Button>
{(mode === "create" || (mode === "edit" && !isGlobal)) ? ( {mode === "create" || (mode === "edit" && !isGlobal) ? (
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting || !isFormValid} disabled={isSubmitting || !isFormValid}
@ -516,8 +523,10 @@ export function ImageConfigDialog({
<Spinner size="sm" /> <Spinner size="sm" />
{mode === "edit" ? "Saving" : "Creating"} {mode === "edit" ? "Saving" : "Creating"}
</> </>
) : mode === "edit" ? (
"Save Changes"
) : ( ) : (
mode === "edit" ? "Save Changes" : "Create & Use" "Create & Use"
)} )}
</Button> </Button>
) : isAutoMode ? ( ) : isAutoMode ? (

View file

@ -200,7 +200,9 @@ export function ModelConfigDialog({
"flex flex-col overflow-hidden" "flex flex-col overflow-hidden"
)} )}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => { if (e.key === "Escape") onOpenChange(false); }} onKeyDown={(e) => {
if (e.key === "Escape") onOpenChange(false);
}}
> >
{/* Header */} {/* Header */}
<div className="flex items-start justify-between px-6 pt-6 pb-4"> <div className="flex items-start justify-between px-6 pt-6 pb-4">
@ -225,7 +227,9 @@ export function ModelConfigDialog({
</div> </div>
<p className="text-sm text-muted-foreground">{getSubtitle()}</p> <p className="text-sm text-muted-foreground">{getSubtitle()}</p>
{config && !isAutoMode && mode !== "create" && ( {config && !isAutoMode && mode !== "create" && (
<p className="text-xs font-mono text-muted-foreground/70">{config.model_name}</p> <p className="text-xs font-mono text-muted-foreground/70">
{config.model_name}
</p>
)} )}
</div> </div>
<Button <Button
@ -330,7 +334,6 @@ export function ModelConfigDialog({
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : isGlobal && config ? ( ) : isGlobal && config ? (
<div className="space-y-6"> <div className="space-y-6">
@ -401,7 +404,6 @@ export function ModelConfigDialog({
</> </>
)} )}
</div> </div>
</div> </div>
) : config ? ( ) : config ? (
<LLMConfigForm <LLMConfigForm
@ -431,16 +433,16 @@ export function ModelConfigDialog({
{/* Fixed footer */} {/* Fixed footer */}
<div className="shrink-0 px-6 py-4 flex items-center justify-end gap-3"> <div className="shrink-0 px-6 py-4 flex items-center justify-end gap-3">
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
disabled={isSubmitting} disabled={isSubmitting}
className="text-sm h-9" className="text-sm h-9"
> >
Cancel Cancel
</Button> </Button>
{(mode === "create" || (!isGlobal && !isAutoMode && config)) ? ( {mode === "create" || (!isGlobal && !isAutoMode && config) ? (
<Button <Button
type="submit" type="submit"
form="model-config-form" form="model-config-form"
@ -452,8 +454,10 @@ export function ModelConfigDialog({
<Spinner size="sm" /> <Spinner size="sm" />
{mode === "edit" ? "Saving" : "Creating"} {mode === "edit" ? "Saving" : "Creating"}
</> </>
) : mode === "edit" ? (
"Save Changes"
) : ( ) : (
mode === "edit" ? "Save Changes" : "Create & Use" "Create & Use"
)} )}
</Button> </Button>
) : isAutoMode ? ( ) : isAutoMode ? (

View file

@ -578,9 +578,7 @@ function RolesContent({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<DropdownMenuItem <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
onSelect={(e) => e.preventDefault()}
>
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
Delete Role Delete Role
</DropdownMenuItem> </DropdownMenuItem>

View file

@ -173,14 +173,9 @@ export function LLMConfigForm({
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm"> <FormLabel className="text-xs sm:text-sm">Configuration Name</FormLabel>
Configuration Name
</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder="e.g., My GPT-4 Agent" {...field} />
placeholder="e.g., My GPT-4 Agent"
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -280,17 +275,20 @@ export function LLMConfigForm({
variant="outline" variant="outline"
role="combobox" role="combobox"
aria-expanded={modelComboboxOpen} aria-expanded={modelComboboxOpen}
className={cn( className={cn(
"w-full justify-between font-normal bg-transparent", "w-full justify-between font-normal bg-transparent",
!field.value && "text-muted-foreground" !field.value && "text-muted-foreground"
)} )}
> >
{field.value || "Select or type model name"} {field.value || "Select or type model name"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0 bg-muted dark:border-neutral-700" align="start"> <PopoverContent
className="w-full p-0 bg-muted dark:border-neutral-700"
align="start"
>
<Command shouldFilter={false} className="bg-transparent"> <Command shouldFilter={false} className="bg-transparent">
<CommandInput <CommandInput
placeholder={selectedProvider?.example || "Type model name..."} placeholder={selectedProvider?.example || "Type model name..."}
@ -362,9 +360,7 @@ export function LLMConfigForm({
name="api_key" name="api_key"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm"> <FormLabel className="text-xs sm:text-sm">API Key</FormLabel>
API Key
</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
@ -488,7 +484,7 @@ export function LLMConfigForm({
type="button" type="button"
className="flex w-full items-center justify-between py-2 text-xs sm:text-sm font-medium text-muted-foreground hover:text-foreground transition-colors" className="flex w-full items-center justify-between py-2 text-xs sm:text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
> >
<span>System Instructions</span> <span>System Instructions</span>
<ChevronDown <ChevronDown
className={cn( className={cn(
"h-4 w-4 transition-transform duration-200", "h-4 w-4 transition-transform duration-200",
@ -589,7 +585,8 @@ export function LLMConfigForm({
</> </>
) : ( ) : (
<> <>
{submitLabel ?? (mode === "edit" ? "Update Configuration" : "Create Configuration")} {submitLabel ??
(mode === "edit" ? "Update Configuration" : "Create Configuration")}
</> </>
)} )}
</Button> </Button>

View file

@ -88,7 +88,10 @@ function Calendar({
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none", "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day defaultClassNames.day
), ),
range_start: cn("rounded-l-md bg-accent dark:bg-neutral-700", defaultClassNames.range_start), range_start: cn(
"rounded-l-md bg-accent dark:bg-neutral-700",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle), range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent dark:bg-neutral-700", defaultClassNames.range_end), range_end: cn("rounded-r-md bg-accent dark:bg-neutral-700", defaultClassNames.range_end),
today: cn( today: cn(

View file

@ -59,7 +59,9 @@ export function FixedToolbarButtons() {
</ToolbarButton> </ToolbarButton>
<ToolbarButton <ToolbarButton
tooltip={<TooltipWithShortcut label="Redo" keys={shortcutKeys("Mod", "Shift", "Z")} />} tooltip={
<TooltipWithShortcut label="Redo" keys={shortcutKeys("Mod", "Shift", "Z")} />
}
onClick={() => { onClick={() => {
editor.redo(); editor.redo();
editor.tf.focus(); editor.tf.focus();
@ -75,11 +77,17 @@ export function FixedToolbarButtons() {
</ToolbarGroup> </ToolbarGroup>
<ToolbarGroup> <ToolbarGroup>
<MarkToolbarButton nodeType={KEYS.bold} tooltip={<TooltipWithShortcut label="Bold" keys={shortcutKeys("Mod", "B")} />}> <MarkToolbarButton
nodeType={KEYS.bold}
tooltip={<TooltipWithShortcut label="Bold" keys={shortcutKeys("Mod", "B")} />}
>
<BoldIcon /> <BoldIcon />
</MarkToolbarButton> </MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.italic} tooltip={<TooltipWithShortcut label="Italic" keys={shortcutKeys("Mod", "I")} />}> <MarkToolbarButton
nodeType={KEYS.italic}
tooltip={<TooltipWithShortcut label="Italic" keys={shortcutKeys("Mod", "I")} />}
>
<ItalicIcon /> <ItalicIcon />
</MarkToolbarButton> </MarkToolbarButton>
@ -92,18 +100,28 @@ export function FixedToolbarButtons() {
<MarkToolbarButton <MarkToolbarButton
nodeType={KEYS.strikethrough} nodeType={KEYS.strikethrough}
tooltip={<TooltipWithShortcut label="Strikethrough" keys={shortcutKeys("Mod", "Shift", "X")} />} tooltip={
<TooltipWithShortcut
label="Strikethrough"
keys={shortcutKeys("Mod", "Shift", "X")}
/>
}
> >
<StrikethroughIcon /> <StrikethroughIcon />
</MarkToolbarButton> </MarkToolbarButton>
<MarkToolbarButton nodeType={KEYS.code} tooltip={<TooltipWithShortcut label="Code" keys={shortcutKeys("Mod", "E")} />}> <MarkToolbarButton
nodeType={KEYS.code}
tooltip={<TooltipWithShortcut label="Code" keys={shortcutKeys("Mod", "E")} />}
>
<Code2Icon /> <Code2Icon />
</MarkToolbarButton> </MarkToolbarButton>
<MarkToolbarButton <MarkToolbarButton
nodeType={KEYS.highlight} nodeType={KEYS.highlight}
tooltip={<TooltipWithShortcut label="Highlight" keys={shortcutKeys("Mod", "Shift", "H")} />} tooltip={
<TooltipWithShortcut label="Highlight" keys={shortcutKeys("Mod", "Shift", "H")} />
}
> >
<HighlighterIcon /> <HighlighterIcon />
</MarkToolbarButton> </MarkToolbarButton>
@ -122,7 +140,13 @@ export function FixedToolbarButtons() {
{!readOnly && onSave && hasUnsavedChanges && ( {!readOnly && onSave && hasUnsavedChanges && (
<ToolbarGroup> <ToolbarGroup>
<ToolbarButton <ToolbarButton
tooltip={isSaving ? "Saving..." : <TooltipWithShortcut label="Save" keys={shortcutKeys("Mod", "S")} />} tooltip={
isSaving ? (
"Saving..."
) : (
<TooltipWithShortcut label="Save" keys={shortcutKeys("Mod", "S")} />
)
}
onClick={onSave} onClick={onSave}
disabled={isSaving} disabled={isSaving}
className="bg-primary text-primary-foreground hover:bg-primary/90" className="bg-primary text-primary-foreground hover:bg-primary/90"

View file

@ -47,7 +47,9 @@ export function useComments({ messageId, enabled = true }: UseCommentsOptions) {
if (_batchInflight && _batchTargetIds.has(messageId)) { if (_batchInflight && _batchTargetIds.has(messageId)) {
await _batchInflight; await _batchInflight;
const cached = queryClient.getQueryData<GetCommentsResponse>(cacheKeys.comments.byMessage(messageId)); const cached = queryClient.getQueryData<GetCommentsResponse>(
cacheKeys.comments.byMessage(messageId)
);
if (cached) return cached; if (cached) return cached;
} }

View file

@ -66,7 +66,7 @@ export function useDocumentsProcessing(searchSpaceId: number | null): boolean {
live?: { live?: {
query: <T>( query: <T>(
sql: string, sql: string,
params?: (number | string)[], params?: (number | string)[]
) => Promise<{ ) => Promise<{
subscribe: (cb: (result: { rows: T[] }) => void) => void; subscribe: (cb: (result: { rows: T[] }) => void) => void;
unsubscribe?: () => void; unsubscribe?: () => void;
@ -80,7 +80,7 @@ export function useDocumentsProcessing(searchSpaceId: number | null): boolean {
`SELECT COUNT(*) as count FROM documents `SELECT COUNT(*) as count FROM documents
WHERE search_space_id = $1 WHERE search_space_id = $1
AND (status->>'state' = 'pending' OR status->>'state' = 'processing')`, AND (status->>'state' = 'pending' OR status->>'state' = 'processing')`,
[spaceId], [spaceId]
); );
if (!mounted) { if (!mounted) {

View file

@ -356,8 +356,11 @@ export function useDocuments(
const prevIds = new Set(prev.map((d) => d.id)); const prevIds = new Set(prev.map((d) => d.id));
const newItems = filterNewElectricItems( const newItems = filterNewElectricItems(
validItems, liveIds, prevIds, validItems,
electricBaselineIdsRef, newestApiTimestampRef.current, liveIds,
prevIds,
electricBaselineIdsRef,
newestApiTimestampRef.current
).map(electricToDisplayDoc); ).map(electricToDisplayDoc);
// Update existing docs (status changes, title edits) // Update existing docs (status changes, title edits)

View file

@ -6,7 +6,11 @@ import { notificationsApiService } from "@/lib/apis/notifications-api.service";
import { filterNewElectricItems, getNewestTimestamp } from "@/lib/electric/baseline"; import { filterNewElectricItems, getNewestTimestamp } from "@/lib/electric/baseline";
import { useElectricClient } from "@/lib/electric/context"; import { useElectricClient } from "@/lib/electric/context";
export type { InboxItem, InboxItemTypeEnum, NotificationCategory } from "@/contracts/types/inbox.types"; export type {
InboxItem,
InboxItemTypeEnum,
NotificationCategory,
} from "@/contracts/types/inbox.types";
const INITIAL_PAGE_SIZE = 50; const INITIAL_PAGE_SIZE = 50;
const SCROLL_PAGE_SIZE = 30; const SCROLL_PAGE_SIZE = 30;
@ -14,7 +18,8 @@ const SYNC_WINDOW_DAYS = 4;
const CATEGORY_TYPE_SQL: Record<NotificationCategory, string> = { const CATEGORY_TYPE_SQL: Record<NotificationCategory, string> = {
comments: "AND type IN ('new_mention', 'comment_reply')", comments: "AND type IN ('new_mention', 'comment_reply')",
status: "AND type IN ('connector_indexing', 'connector_deletion', 'document_processing', 'page_limit_exceeded')", status:
"AND type IN ('connector_indexing', 'connector_deletion', 'document_processing', 'page_limit_exceeded')",
}; };
/** /**
@ -52,7 +57,7 @@ function getSyncCutoffDate(): string {
export function useInbox( export function useInbox(
userId: string | null, userId: string | null,
searchSpaceId: number | null, searchSpaceId: number | null,
category: NotificationCategory, category: NotificationCategory
) { ) {
const electricClient = useElectricClient(); const electricClient = useElectricClient();
@ -119,7 +124,9 @@ export function useInbox(
}; };
fetchInitialData(); fetchInitialData();
return () => { cancelled = true; }; return () => {
cancelled = true;
};
}, [userId, searchSpaceId, category]); }, [userId, searchSpaceId, category]);
// EFFECT 2: Electric sync (shared shape) + per-instance type-filtered live queries // EFFECT 2: Electric sync (shared shape) + per-instance type-filtered live queries
@ -135,11 +142,19 @@ export function useInbox(
async function setupElectricRealtime() { async function setupElectricRealtime() {
// Clean up previous live queries (NOT the sync shape — it's shared) // Clean up previous live queries (NOT the sync shape — it's shared)
if (liveQueryRef.current) { if (liveQueryRef.current) {
try { liveQueryRef.current.unsubscribe?.(); } catch { /* PGlite may be closed */ } try {
liveQueryRef.current.unsubscribe?.();
} catch {
/* PGlite may be closed */
}
liveQueryRef.current = null; liveQueryRef.current = null;
} }
if (unreadLiveQueryRef.current) { if (unreadLiveQueryRef.current) {
try { unreadLiveQueryRef.current.unsubscribe?.(); } catch { /* PGlite may be closed */ } try {
unreadLiveQueryRef.current.unsubscribe?.();
} catch {
/* PGlite may be closed */
}
unreadLiveQueryRef.current = null; unreadLiveQueryRef.current = null;
} }
@ -207,8 +222,11 @@ export function useInbox(
const prevIds = new Set(prev.map((d) => d.id)); const prevIds = new Set(prev.map((d) => d.id));
const newItems = filterNewElectricItems( const newItems = filterNewElectricItems(
validItems, liveIds, prevIds, validItems,
electricBaselineIdsRef, newestApiTimestampRef.current, liveIds,
prevIds,
electricBaselineIdsRef,
newestApiTimestampRef.current
); );
let updated = prev.map((item) => { let updated = prev.map((item) => {
@ -267,7 +285,10 @@ export function useInbox(
AND read = false AND read = false
${typeFilter}`; ${typeFilter}`;
const countLiveQuery = await db.live.query<{ count: number | string }>(countQuery, [uid, spaceId]); const countLiveQuery = await db.live.query<{ count: number | string }>(countQuery, [
uid,
spaceId,
]);
if (!mounted) { if (!mounted) {
countLiveQuery.unsubscribe?.(); countLiveQuery.unsubscribe?.();
@ -293,11 +314,19 @@ export function useInbox(
mounted = false; mounted = false;
// Only clean up live queries — sync shape is shared across instances // Only clean up live queries — sync shape is shared across instances
if (liveQueryRef.current) { if (liveQueryRef.current) {
try { liveQueryRef.current.unsubscribe?.(); } catch { /* PGlite may be closed */ } try {
liveQueryRef.current.unsubscribe?.();
} catch {
/* PGlite may be closed */
}
liveQueryRef.current = null; liveQueryRef.current = null;
} }
if (unreadLiveQueryRef.current) { if (unreadLiveQueryRef.current) {
try { unreadLiveQueryRef.current.unsubscribe?.(); } catch { /* PGlite may be closed */ } try {
unreadLiveQueryRef.current.unsubscribe?.();
} catch {
/* PGlite may be closed */
}
unreadLiveQueryRef.current = null; unreadLiveQueryRef.current = null;
} }
}; };

View file

@ -38,7 +38,7 @@ export function filterNewElectricItems<T extends { id: number; created_at: strin
liveIds: Set<number>, liveIds: Set<number>,
prevIds: Set<number>, prevIds: Set<number>,
baselineRef: MutableRefObject<Set<number> | null>, baselineRef: MutableRefObject<Set<number> | null>,
newestApiTimestamp: string | null, newestApiTimestamp: string | null
): T[] { ): T[] {
if (baselineRef.current === null) { if (baselineRef.current === null) {
baselineRef.current = new Set(liveIds); baselineRef.current = new Set(liveIds);