mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
chore: ran linting
This commit is contained in:
parent
37e1995546
commit
f8b0e946ce
31 changed files with 768 additions and 754 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,3 @@ export function AnnouncementsEmptyState() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,3 @@ export function AnnouncementsSidebar({
|
||||||
</SidebarSlideOutPanel>
|
</SidebarSlideOutPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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 ? (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue