diff --git a/surfsense_backend/app/routes/notifications_routes.py b/surfsense_backend/app/routes/notifications_routes.py index b77a39249..4f80c6529 100644 --- a/surfsense_backend/app/routes/notifications_routes.py +++ b/surfsense_backend/app/routes/notifications_routes.py @@ -197,10 +197,9 @@ async def list_notifications( # Filter by search query (case-insensitive title/message search) if search: search_term = f"%{search}%" - search_filter = ( - Notification.title.ilike(search_term) - | Notification.message.ilike(search_term) - ) + search_filter = Notification.title.ilike( + search_term + ) | Notification.message.ilike(search_term) query = query.where(search_filter) count_query = count_query.where(search_filter) diff --git a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py index dfbfea432..81c5dbba2 100644 --- a/surfsense_backend/app/tasks/celery_tasks/document_tasks.py +++ b/surfsense_backend/app/tasks/celery_tasks/document_tasks.py @@ -1005,9 +1005,7 @@ async def _process_circleback_meeting( # Start Redis heartbeat for stale task detection _start_heartbeat(notification.id) - heartbeat_task = asyncio.create_task( - _run_heartbeat_loop(notification.id) - ) + heartbeat_task = asyncio.create_task(_run_heartbeat_loop(notification.id)) log_entry = await task_logger.log_task_start( task_name="process_circleback_meeting", diff --git a/surfsense_backend/app/tasks/celery_tasks/stale_notification_cleanup_task.py b/surfsense_backend/app/tasks/celery_tasks/stale_notification_cleanup_task.py index c47652d2c..f3bbddee0 100644 --- a/surfsense_backend/app/tasks/celery_tasks/stale_notification_cleanup_task.py +++ b/surfsense_backend/app/tasks/celery_tasks/stale_notification_cleanup_task.py @@ -45,9 +45,8 @@ _redis_client: redis.Redis | None = None # Error messages shown to users when tasks are interrupted STALE_SYNC_ERROR_MESSAGE = "Sync was interrupted unexpectedly. Please retry." -STALE_PROCESSING_ERROR_MESSAGE = ( - "Syncing was interrupted unexpectedly. Please retry." -) +STALE_PROCESSING_ERROR_MESSAGE = "Syncing was interrupted unexpectedly. Please retry." + def get_redis_client() -> redis.Redis: """Get or create Redis client for heartbeat checking.""" @@ -310,9 +309,7 @@ async def _cleanup_stale_document_processing_notifications(): in_progress_rows = result.fetchall() if not in_progress_rows: - logger.debug( - "No in-progress document processing notifications found" - ) + logger.debug("No in-progress document processing notifications found") return # Check which ones are missing heartbeat keys in Redis @@ -389,9 +386,7 @@ async def _cleanup_stale_document_processing_notifications(): await session.rollback() -async def _cleanup_stuck_non_connector_documents( - session, document_ids: list[int] -): +async def _cleanup_stuck_non_connector_documents(session, document_ids: list[int]): """ Mark specific non-connector documents stuck in pending/processing as failed. diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx index 8c142edcc..6421db92f 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell.tsx @@ -372,11 +372,11 @@ export function DocumentsTableShell({ )} - {columnVisibility.status && ( - - - - )} + {columnVisibility.status && ( + + + + )} Actions @@ -544,14 +544,14 @@ export function DocumentsTableShell({ )} - {columnVisibility.status && ( - - - - Status - - - )} + {columnVisibility.status && ( + + + + Status + + + )} Actions @@ -647,11 +647,11 @@ export function DocumentsTableShell({ )} - {columnVisibility.status && ( - - - - )} + {columnVisibility.status && ( + + + + )} )} {activeSection === "models" && } - {activeSection === "roles" && } - {activeSection === "image-models" && ( - - )} - {activeSection === "prompts" && } + {activeSection === "roles" && } + {activeSection === "image-models" && ( + + )} + {activeSection === "prompts" && } {activeSection === "public-links" && ( )} diff --git a/surfsense_web/components/Logo.tsx b/surfsense_web/components/Logo.tsx index 9f5915777..dfb53eabe 100644 --- a/surfsense_web/components/Logo.tsx +++ b/surfsense_web/components/Logo.tsx @@ -4,7 +4,13 @@ import Image from "next/image"; import Link from "next/link"; import { cn } from "@/lib/utils"; -export const Logo = ({ className, disableLink = false }: { className?: string; disableLink?: boolean }) => { +export const Logo = ({ + className, + disableLink = false, +}: { + className?: string; + disableLink?: boolean; +}) => { const image = ( { const UserActionBar: FC = () => { const isThreadRunning = useAssistantState(({ thread }) => thread.isRunning); - + // Get current message ID const currentMessageId = useAssistantState(({ message }) => message?.id); - + // Find the last user message ID in the thread (computed once, memoized by selector) const lastUserMessageId = useAssistantState(({ thread }) => { const messages = thread.messages; @@ -117,7 +117,7 @@ const UserActionBar: FC = () => { // Simple comparison - no iteration needed per message const isLastUserMessage = currentMessageId === lastUserMessageId; - + // Show edit button only on the last user message and when thread is not running const canEdit = isLastUserMessage && !isThreadRunning; diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 66d2f419a..ead017a3e 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -526,7 +526,9 @@ export function LayoutDataProvider({ queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); // Invalidate thread detail for breadcrumb update - queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId, "detail", String(chatToRename.id)] }); + queryClient.invalidateQueries({ + queryKey: ["threads", searchSpaceId, "detail", String(chatToRename.id)], + }); } catch (error) { console.error("Error renaming thread:", error); toast.error(tSidebar("error_renaming_chat") || "Failed to rename chat"); diff --git a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx index ba2989145..157a2ae04 100644 --- a/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx +++ b/surfsense_web/components/layout/ui/sidebar/ChatListItem.tsx @@ -1,6 +1,13 @@ "use client"; -import { ArchiveIcon, MessageSquare, MoreHorizontal, PencilIcon, RotateCcwIcon, Trash2 } from "lucide-react"; +import { + ArchiveIcon, + MessageSquare, + MoreHorizontal, + PencilIcon, + RotateCcwIcon, + Trash2, +} from "lucide-react"; import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { @@ -59,26 +66,26 @@ export function ChatListItem({ {t("more_options")} - - {onRename && ( - { - e.stopPropagation(); - onRename(); - }} - > - - {t("rename") || "Rename"} - - )} - {onArchive && ( - { - e.stopPropagation(); - onArchive(); - }} - > - {archived ? ( + + {onRename && ( + { + e.stopPropagation(); + onRename(); + }} + > + + {t("rename") || "Rename"} + + )} + {onArchive && ( + { + e.stopPropagation(); + onArchive(); + }} + > + {archived ? ( <> {t("unarchive") || "Restore"} diff --git a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx index d46651440..3e5521f47 100644 --- a/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/InboxSidebar.tsx @@ -219,7 +219,7 @@ export function InboxSidebar({ // Server-side search query (enabled only when user is typing a search) // Determines which notification types to search based on active tab - const searchTypeFilter = activeTab === "comments" ? "new_mention" as const : undefined; + const searchTypeFilter = activeTab === "comments" ? ("new_mention" as const) : undefined; const { data: searchResponse, isLoading: isSearchLoading } = useQuery({ queryKey: cacheKeys.notifications.search(searchSpaceId, debouncedSearch.trim(), activeTab), queryFn: () => @@ -288,8 +288,10 @@ export function InboxSidebar({ // Pagination switches based on active tab const loading = activeTab === "comments" ? mentions.loading : status.loading; - const loadingMore = activeTab === "comments" ? (mentions.loadingMore ?? false) : (status.loadingMore ?? false); - const hasMore = activeTab === "comments" ? (mentions.hasMore ?? false) : (status.hasMore ?? false); + const loadingMore = + activeTab === "comments" ? (mentions.loadingMore ?? false) : (status.loadingMore ?? false); + const hasMore = + activeTab === "comments" ? (mentions.hasMore ?? false) : (status.hasMore ?? false); const loadMore = activeTab === "comments" ? mentions.loadMore : status.loadMore; // Get unique connector types from status items for filtering @@ -319,9 +321,7 @@ export function InboxSidebar({ // When not searching: use Electric real-time items (fast, local) const filteredItems = useMemo(() => { // In search mode, use API results - let items: InboxItem[] = isSearchMode - ? (searchResponse?.items ?? []) - : displayItems; + let items: InboxItem[] = isSearchMode ? (searchResponse?.items ?? []) : displayItems; // For status tab search results, filter to status-specific types if (isSearchMode && activeTab === "status") { @@ -926,49 +926,49 @@ export function InboxSidebar({ -
- {(isSearchMode ? isSearchLoading : loading) ? ( -
- {activeTab === "comments" - ? /* Comments skeleton: avatar + two-line text + time */ - [85, 60, 90, 70, 50, 75].map((titleWidth, i) => ( -
- -
- - +
+ {(isSearchMode ? isSearchLoading : loading) ? ( +
+ {activeTab === "comments" + ? /* Comments skeleton: avatar + two-line text + time */ + [85, 60, 90, 70, 50, 75].map((titleWidth, i) => ( +
+ +
+ + +
+
- -
- )) - : /* Status skeleton: status icon circle + two-line text + time */ - [75, 90, 55, 80, 65, 85].map((titleWidth, i) => ( -
- -
- - + )) + : /* Status skeleton: status icon circle + two-line text + time */ + [75, 90, 55, 80, 65, 85].map((titleWidth, i) => ( +
+ +
+ + +
+
+ + +
-
- - -
-
- ))} -
- ) : filteredItems.length > 0 ? ( -
- {filteredItems.map((item, index) => { - const isMarkingAsRead = markingAsReadId === item.id; - // Place prefetch trigger on 5th item from end (only when not searching) - const isPrefetchTrigger = - !isSearchMode && hasMore && index === filteredItems.length - 5; + ))} +
+ ) : filteredItems.length > 0 ? ( +
+ {filteredItems.map((item, index) => { + const isMarkingAsRead = markingAsReadId === item.id; + // Place prefetch trigger on 5th item from end (only when not searching) + const isPrefetchTrigger = + !isSearchMode && hasMore && index === filteredItems.length - 5; return (
); })} - {/* Fallback trigger at the very end if less than 5 items and not searching */} - {!isSearchMode && filteredItems.length < 5 && hasMore && ( -
- )} - {/* Loading more skeletons at the bottom during infinite scroll */} - {loadingMore && ( - activeTab === "comments" - ? [80, 60, 90].map((titleWidth, i) => ( -
- -
- - -
- -
- )) - : [70, 85, 55].map((titleWidth, i) => ( -
- -
- - -
-
- - -
-
- )) - )} + {/* Fallback trigger at the very end if less than 5 items and not searching */} + {!isSearchMode && filteredItems.length < 5 && hasMore && ( +
+ )} + {/* Loading more skeletons at the bottom during infinite scroll */} + {loadingMore && + (activeTab === "comments" + ? [80, 60, 90].map((titleWidth, i) => ( +
+ +
+ + +
+ +
+ )) + : [70, 85, 55].map((titleWidth, i) => ( +
+ +
+ + +
+
+ + +
+
+ )))} +
+ ) : isSearchMode ? ( +
+ +

+ {t("no_results_found") || "No results found"} +

+

+ {t("try_different_search") || "Try a different search term"} +

- ) : isSearchMode ? ( -
- -

- {t("no_results_found") || "No results found"} -

-

- {t("try_different_search") || "Try a different search term"} -

-
) : (
{activeTab === "comments" ? ( diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 38b3028d2..997482ed3 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -1,6 +1,16 @@ "use client"; -import { Check, ChevronUp, Languages, Laptop, Loader2, LogOut, Moon, Settings, Sun } from "lucide-react"; +import { + Check, + ChevronUp, + Languages, + Laptop, + Loader2, + LogOut, + Moon, + Settings, + Sun, +} from "lucide-react"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index 2e04fa3ba..c9e693b21 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -155,7 +155,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS Share settings - - {/* Globe indicator when public snapshots exist - clicks to settings */} + {/* Globe indicator when public snapshots exist - clicks to settings */} {hasPublicSnapshots && ( diff --git a/surfsense_web/components/new-chat/image-config-sidebar.tsx b/surfsense_web/components/new-chat/image-config-sidebar.tsx index 18f98acb7..be84b0b22 100644 --- a/surfsense_web/components/new-chat/image-config-sidebar.tsx +++ b/surfsense_web/components/new-chat/image-config-sidebar.tsx @@ -179,7 +179,17 @@ export function ImageConfigSidebar({ } finally { setIsSubmitting(false); } - }, [mode, isGlobal, config, formData, searchSpaceId, createConfig, updateConfig, updatePreferences, onOpenChange]); + }, [ + mode, + isGlobal, + config, + formData, + searchSpaceId, + createConfig, + updateConfig, + updatePreferences, + onOpenChange, + ]); const handleUseGlobalConfig = useCallback(async () => { if (!config || !isGlobal) return; @@ -297,11 +307,16 @@ export function ImageConfigSidebar({ - Auto mode distributes image generation requests across all configured providers for optimal performance and rate limit protection. + Auto mode distributes image generation requests across all configured + providers for optimal performance and rate limit protection.
- -
@@ -379,7 +410,9 @@ export function ImageConfigSidebar({ setFormData((p) => ({ ...p, description: e.target.value }))} + onChange={(e) => + setFormData((p) => ({ ...p, description: e.target.value })) + } />
@@ -390,7 +423,9 @@ export function ImageConfigSidebar({ setFormData((p) => ({ ...p, model_name: e.target.value }))} + onChange={(e) => + setFormData((p) => ({ ...p, model_name: e.target.value })) + } /> )}
@@ -489,14 +543,20 @@ export function ImageConfigSidebar({ setFormData((p) => ({ ...p, api_version: e.target.value }))} + onChange={(e) => + setFormData((p) => ({ ...p, api_version: e.target.value })) + } />
)} {/* Actions */}
-
- {filteredGlobal.map((config) => { - const isSelected = currentConfig?.id === config.id; - const isAuto = "is_auto_mode" in config && config.is_auto_mode; - return ( - handleSelect(config.id)} - className={cn( - "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", - isSelected && "bg-accent/80", - isAuto && "border border-violet-200 dark:border-violet-800/50" - )} - > -
-
- {isAuto ? ( - - ) : ( - + {filteredGlobal.map((config) => { + const isSelected = currentConfig?.id === config.id; + const isAuto = "is_auto_mode" in config && config.is_auto_mode; + return ( + handleSelect(config.id)} + className={cn( + "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", + isSelected && "bg-accent/80", + isAuto && "border border-violet-200 dark:border-violet-800/50" + )} + > +
+
+ {isAuto ? ( + + ) : ( + + )} +
+
+
+ {config.name} + {isAuto && ( + + Recommended + + )} + {isSelected && } +
+ + {isAuto ? "Auto load balancing" : config.model_name} + +
+ {onEdit && ( + { + e.stopPropagation(); + setOpen(false); + onEdit(config, true); + }} + /> )}
-
-
- {config.name} - {isAuto && ( - - Recommended - - )} - {isSelected && } -
- - {isAuto ? "Auto load balancing" : config.model_name} - -
- {onEdit && ( - { - e.stopPropagation(); - setOpen(false); - onEdit(config, true); - }} - /> - )} -
- - ); - })} + + ); + })} )} @@ -290,51 +289,49 @@ export function ImageModelSelector({ className, onAddNew, onEdit }: ImageModelSe Your Image Models
- {filteredUser.map((config) => { - const isSelected = currentConfig?.id === config.id; - return ( - handleSelect(config.id)} - className={cn( - "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", - isSelected && "bg-accent/80" - )} - > -
-
- -
-
-
- {config.name} - {isSelected && ( - - )} -
- - {config.model_name} - -
- {onEdit && ( - + {filteredUser.map((config) => { + const isSelected = currentConfig?.id === config.id; + return ( + handleSelect(config.id)} + className={cn( + "mx-2 rounded-lg mb-1 cursor-pointer group transition-all hover:bg-accent/50", + isSelected && "bg-accent/80" )} -
-
- ); - })} + > +
+
+ +
+
+
+ {config.name} + {isSelected && } +
+ + {config.model_name} + +
+ {onEdit && ( + + )} +
+
+ ); + })} )} diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index 148028df2..ec1143e04 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -392,8 +392,8 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp )} - {/* Add New Config Button */} -
+ {/* Add New Config Button */} +
-
@@ -409,7 +456,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {

Your Image Models

- @@ -435,7 +485,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {userConfigs?.map((config) => ( - +
@@ -448,8 +503,13 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) {
-

{config.name}

- +

+ {config.name} +

+ {config.provider}
@@ -457,7 +517,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { {config.model_name} {config.description && ( -

{config.description}

+

+ {config.description} +

)}
@@ -469,7 +531,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { - @@ -479,7 +546,12 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { - @@ -501,15 +573,30 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { )} {/* Create/Edit Dialog */} - { if (!open) { setIsDialogOpen(false); setEditingConfig(null); resetForm(); } }}> + { + if (!open) { + setIsDialogOpen(false); + setEditingConfig(null); + resetForm(); + } + }} + > - {editingConfig ? : } + {editingConfig ? ( + + ) : ( + + )} {editingConfig ? "Edit Image Model" : "Add Image Model"} - {editingConfig ? "Update your image generation model" : "Configure a new image generation model (DALL-E 3, GPT Image 1, etc.)"} + {editingConfig + ? "Update your image generation model" + : "Configure a new image generation model (DALL-E 3, GPT Image 1, etc.)"} @@ -541,7 +628,9 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { handleRoleAssignment(`${key}_llm_id`, value)} - > - - - - - - Unassigned - +
+ + -
+ {/* Custom Configurations */} + {newLLMConfigs.length > 0 && ( + <> +
+ Your Configurations +
+ {newLLMConfigs + .filter( + (config) => config.id && config.id.toString().trim() !== "" + ) + .map((config) => ( + +
+ + {config.provider} + + {config.name} + + ({config.model_name}) + +
+
+ ))} + + )} +
+ +
{assignedConfig && (
{isGenerated && } diff --git a/surfsense_web/contracts/types/new-llm-config.types.ts b/surfsense_web/contracts/types/new-llm-config.types.ts index 3f0d39e5a..b99df1022 100644 --- a/surfsense_web/contracts/types/new-llm-config.types.ts +++ b/surfsense_web/contracts/types/new-llm-config.types.ts @@ -213,9 +213,7 @@ export const getImageGenConfigsResponse = z.array(imageGenerationConfig); export const updateImageGenConfigRequest = z.object({ id: z.number(), - data: imageGenerationConfig - .omit({ id: true, created_at: true, search_space_id: true }) - .partial(), + data: imageGenerationConfig.omit({ id: true, created_at: true, search_space_id: true }).partial(), }); export const updateImageGenConfigResponse = imageGenerationConfig; diff --git a/surfsense_web/lib/apis/image-gen-config-api.service.ts b/surfsense_web/lib/apis/image-gen-config-api.service.ts index 84aeed3d8..379edfa53 100644 --- a/surfsense_web/lib/apis/image-gen-config-api.service.ts +++ b/surfsense_web/lib/apis/image-gen-config-api.service.ts @@ -32,11 +32,9 @@ class ImageGenConfigApiService { const msg = parsed.error.issues.map((i) => i.message).join(", "); throw new ValidationError(`Invalid request: ${msg}`); } - return baseApiService.post( - `/api/v1/image-generation-configs`, - createImageGenConfigResponse, - { body: parsed.data } - ); + return baseApiService.post(`/api/v1/image-generation-configs`, createImageGenConfigResponse, { + body: parsed.data, + }); }; /**