diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx index 318c2836b..961ca468e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/logs/loading.tsx @@ -4,133 +4,130 @@ import { motion } from "motion/react"; import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { - return ( - - {/* Summary Dashboard Skeleton */} - - {[...Array(4)].map((_, i) => ( -
-
- - -
-
- - -
-
- ))} -
+ return ( + + {/* Summary Dashboard Skeleton */} + + {[...Array(4)].map((_, i) => ( +
+
+ + +
+
+ + +
+
+ ))} +
- {/* Header Section Skeleton */} - -
- - -
- -
+ {/* Header Section Skeleton */} + +
+ + +
+ +
- {/* Filters Skeleton */} - -
- - - - -
-
+ {/* Filters Skeleton */} + +
+ + + + +
+
- {/* Table Skeleton */} - - {/* Table Header */} -
- - - - - - - -
+ {/* Table Skeleton */} + + {/* Table Header */} +
+ + + + + + + +
- {/* Table Rows */} - {[...Array(6)].map((_, i) => ( -
- - - -
- - -
-
- - -
-
- - -
- -
- ))} -
+ {/* Table Rows */} + {[...Array(6)].map((_, i) => ( +
+ + + +
+ + +
+
+ + +
+
+ + +
+ +
+ ))} +
- {/* Pagination Skeleton */} -
- - - - + {/* Pagination Skeleton */} +
+ + + + - - - + + + -
- - - - -
-
- - ); +
+ + + + +
+
+
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/more-pages/loading.tsx b/surfsense_web/app/dashboard/[search_space_id]/more-pages/loading.tsx index 9a0c45f3f..ccb3b35e3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/more-pages/loading.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/more-pages/loading.tsx @@ -1,10 +1,10 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { - return ( -
- - -
- ); + return ( +
+ + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index 4c04de724..e173cfdf2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -1527,9 +1527,7 @@ export default function NewChatPage() { // Show loading state only when loading an existing thread if (isInitializing) { - return ( - - ); + return ; } // Show error state only if we tried to load an existing thread but failed @@ -1565,4 +1563,4 @@ export default function NewChatPage() { ); -} \ No newline at end of file +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx index 1f47fb95a..3e390983b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/loading.tsx @@ -1,45 +1,45 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function Loading() { - return ( -
-
- {/* User message */} -
- -
+ return ( +
+
+ {/* User message */} +
+ +
- {/* Assistant message */} -
- - - -
+ {/* Assistant message */} +
+ + + +
- {/* User message */} -
- -
+ {/* User message */} +
+ +
- {/* Assistant message */} -
- - - -
+ {/* Assistant message */} +
+ + + +
- {/* User message */} -
- -
-
+ {/* User message */} +
+ +
+
- {/* Input bar */} -
-
- -
-
-
- ); + {/* Input bar */} +
+
+ +
+
+
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/page.tsx index 516d7610b..69aa47bc3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/page.tsx @@ -1,10 +1,10 @@ import { redirect } from "next/navigation"; export default async function SearchSpaceDashboardPage({ - params, + params, }: { - params: Promise<{ search_space_id: string }>; + params: Promise<{ search_space_id: string }>; }) { - const { search_space_id } = await params; - redirect(`/dashboard/${search_space_id}/new-chat`); + const { search_space_id } = await params; + redirect(`/dashboard/${search_space_id}/new-chat`); } diff --git a/surfsense_web/app/error.tsx b/surfsense_web/app/error.tsx index d9244fc7c..7bbd74e0e 100644 --- a/surfsense_web/app/error.tsx +++ b/surfsense_web/app/error.tsx @@ -3,11 +3,11 @@ import posthog from "posthog-js"; import { useEffect } from "react"; -export default function Error({ +export default function ErrorPage({ error, reset, }: { - error: Error & { digest?: string }; + error: globalThis.Error & { digest?: string }; reset: () => void; }) { useEffect(() => { diff --git a/surfsense_web/app/global-error.tsx b/surfsense_web/app/global-error.tsx index 6707d8dea..dc4763c1e 100644 --- a/surfsense_web/app/global-error.tsx +++ b/surfsense_web/app/global-error.tsx @@ -6,27 +6,25 @@ import { useEffect } from "react"; import { Button } from "@/components/ui/button"; export default function GlobalError({ - error, - reset, + error, + reset, }: { - error: Error & { digest?: string }; - reset: () => void; + error: Error & { digest?: string }; + reset: () => void; }) { - useEffect(() => { - posthog.captureException(error); - }, [error]); + useEffect(() => { + posthog.captureException(error); + }, [error]); - return ( - - -
-

Something went wrong

-

- An unexpected error occurred. -

- -
- - - ); + return ( + + +
+

Something went wrong

+

An unexpected error occurred.

+ +
+ + + ); } diff --git a/surfsense_web/app/public/[token]/page.tsx b/surfsense_web/app/public/[token]/page.tsx index 10cd19732..ce8dc94dd 100644 --- a/surfsense_web/app/public/[token]/page.tsx +++ b/surfsense_web/app/public/[token]/page.tsx @@ -1,11 +1,7 @@ import { PublicChatView } from "@/components/public-chat/public-chat-view"; -export default async function PublicChatPage({ - params, -}: { - params: Promise<{ token: string }>; -}) { - const { token } = await params; +export default async function PublicChatPage({ params }: { params: Promise<{ token: string }> }) { + const { token } = await params; - return ; + return ; } diff --git a/surfsense_web/atoms/documents/folder.atoms.ts b/surfsense_web/atoms/documents/folder.atoms.ts index 86479f564..fe7d556eb 100644 --- a/surfsense_web/atoms/documents/folder.atoms.ts +++ b/surfsense_web/atoms/documents/folder.atoms.ts @@ -9,7 +9,7 @@ import { atomWithStorage } from "jotai/utils"; */ export const expandedFolderIdsAtom = atomWithStorage>( "surfsense:expandedFolderIds", - {}, + {} ); /** diff --git a/surfsense_web/atoms/tabs/tabs.atom.ts b/surfsense_web/atoms/tabs/tabs.atom.ts index 821c8b022..7ba115a95 100644 --- a/surfsense_web/atoms/tabs/tabs.atom.ts +++ b/surfsense_web/atoms/tabs/tabs.atom.ts @@ -41,7 +41,7 @@ export const tabsStateAtom = atomWithStorage( "surfsense:tabs", initialState, sessionStorageAdapter, - { getOnInit: true }, + { getOnInit: true } ); export const tabsAtom = atom((get) => get(tabsStateAtom).tabs); @@ -69,11 +69,7 @@ export const syncChatTabAtom = atom( ( get, set, - { - chatId, - title, - chatUrl, - }: { chatId: number | null; title?: string; chatUrl?: string } + { chatId, title, chatUrl }: { chatId: number | null; title?: string; chatUrl?: string } ) => { const state = get(tabsStateAtom); const tabId = makeChatTabId(chatId); @@ -84,9 +80,7 @@ export const syncChatTabAtom = atom( ...state, activeTabId: tabId, tabs: state.tabs.map((t) => - t.id === tabId - ? { ...t, title: title || t.title, chatUrl: chatUrl || t.chatUrl } - : t + t.id === tabId ? { ...t, title: title || t.title, chatUrl: chatUrl || t.chatUrl } : t ), }); return; @@ -161,9 +155,7 @@ export const openDocumentTabAtom = atom( set(tabsStateAtom, { ...state, activeTabId: tabId, - tabs: state.tabs.map((t) => - t.id === tabId ? { ...t, title: title || t.title } : t - ), + tabs: state.tabs.map((t) => (t.id === tabId ? { ...t, title: title || t.title } : t)), }); return; } diff --git a/surfsense_web/components/assistant-ui/inline-citation.tsx b/surfsense_web/components/assistant-ui/inline-citation.tsx index af3edc760..1c9fa6ba4 100644 --- a/surfsense_web/components/assistant-ui/inline-citation.tsx +++ b/surfsense_web/components/assistant-ui/inline-citation.tsx @@ -28,16 +28,14 @@ export const InlineCitation: FC = ({ chunkId, isDocsChunk = url="" isDocsChunk={isDocsChunk} > - setIsOpen(true)} - onKeyDown={(e) => e.key === "Enter" && setIsOpen(true)} className="text-[10px] font-bold bg-primary/80 hover:bg-primary text-primary-foreground rounded-full min-w-4 h-4 px-1 inline-flex items-center justify-center align-super cursor-pointer transition-colors ml-0.5" title={`View source chunk #${chunkId}`} - role="button" - tabIndex={0} > {chunkId} - + ); }; diff --git a/surfsense_web/components/assistant-ui/thread-list.tsx b/surfsense_web/components/assistant-ui/thread-list.tsx index c79c9d1bc..db7704470 100644 --- a/surfsense_web/components/assistant-ui/thread-list.tsx +++ b/surfsense_web/components/assistant-ui/thread-list.tsx @@ -225,17 +225,13 @@ function ThreadListItemComponent({ onDelete, }: ThreadListItemComponentProps) { return ( -
{ - if (e.key === "Enter" || e.key === " ") onClick(); - }} - role="button" - tabIndex={0} >
@@ -274,7 +270,7 @@ function ThreadListItemComponent({ -
+ ); } diff --git a/surfsense_web/components/auth/sign-in-button.tsx b/surfsense_web/components/auth/sign-in-button.tsx index d45f8b410..dd5893deb 100644 --- a/surfsense_web/components/auth/sign-in-button.tsx +++ b/surfsense_web/components/auth/sign-in-button.tsx @@ -8,7 +8,14 @@ import { cn } from "@/lib/utils"; // Official Google "G" logo with brand colors const GoogleLogo = ({ className }: { className?: string }) => ( - + + Google logo & { width: number; height: number; x: string | number; y: string | number; squares?: [number, number][] }) { +export function GridPattern({ + width, + height, + x, + y, + squares, + ...props +}: React.ComponentProps<"svg"> & { + width: number; + height: number; + x: string | number; + y: string | number; + squares?: [number, number][]; +}) { const patternId = useId(); return ( diff --git a/surfsense_web/components/documents/CreateFolderDialog.tsx b/surfsense_web/components/documents/CreateFolderDialog.tsx index b424e92b9..992c5d24c 100644 --- a/surfsense_web/components/documents/CreateFolderDialog.tsx +++ b/surfsense_web/components/documents/CreateFolderDialog.tsx @@ -45,7 +45,7 @@ export function CreateFolderDialog({ onConfirm(trimmed); onOpenChange(false); }, - [name, onConfirm, onOpenChange], + [name, onConfirm, onOpenChange] ); const isSubfolder = !!parentFolderName; diff --git a/surfsense_web/components/documents/DocumentNode.tsx b/surfsense_web/components/documents/DocumentNode.tsx index 9aee3c4d8..4c156d830 100644 --- a/surfsense_web/components/documents/DocumentNode.tsx +++ b/surfsense_web/components/documents/DocumentNode.tsx @@ -1,14 +1,9 @@ "use client"; -import { - Eye, - MoreHorizontal, - Move, - Pencil, - Trash2, -} from "lucide-react"; -import React, { useCallback, useRef } from "react"; +import { Eye, MoreHorizontal, Move, Pencil, Trash2 } from "lucide-react"; +import React, { useCallback } from "react"; import { useDrag } from "react-dnd"; +import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { @@ -25,7 +20,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { getDocumentTypeIcon } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import { cn } from "@/lib/utils"; import { DND_TYPES } from "./FolderNode"; @@ -62,9 +56,7 @@ export const DocumentNode = React.memo(function DocumentNode({ const statusState = doc.status?.state ?? "ready"; const isSelectable = statusState !== "pending" && statusState !== "processing"; const isEditable = - doc.document_type === "NOTE" && - statusState !== "pending" && - statusState !== "processing"; + doc.document_type === "NOTE" && statusState !== "pending" && statusState !== "processing"; const handleCheckChange = useCallback(() => { if (isSelectable) { @@ -78,7 +70,7 @@ export const DocumentNode = React.memo(function DocumentNode({ item: { id: doc.id }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), }), - [doc.id], + [doc.id] ); const isProcessing = statusState === "pending" || statusState === "processing"; @@ -86,15 +78,24 @@ export const DocumentNode = React.memo(function DocumentNode({ return ( + {/* biome-ignore lint/a11y/useSemanticElements: div required for drag ref */}
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleCheckChange(); + } + }} > {isSelectable ? ( @@ -119,7 +120,10 @@ export const DocumentNode = React.memo(function DocumentNode({ {doc.title} - {getDocumentTypeIcon(doc.document_type as DocumentTypeEnum, "h-3.5 w-3.5 text-muted-foreground")} + {getDocumentTypeIcon( + doc.document_type as DocumentTypeEnum, + "h-3.5 w-3.5 text-muted-foreground" + )} @@ -134,10 +138,10 @@ export const DocumentNode = React.memo(function DocumentNode({ - onPreview(doc)}> - - Open - + onPreview(doc)}> + + Open + {isEditable && ( onEdit(doc)}> @@ -163,10 +167,10 @@ export const DocumentNode = React.memo(function DocumentNode({ - onPreview(doc)}> - - Open - + onPreview(doc)}> + + Open + {isEditable && ( onEdit(doc)}> diff --git a/surfsense_web/components/documents/FolderNode.tsx b/surfsense_web/components/documents/FolderNode.tsx index 2f3ab5ad5..03dad83e7 100644 --- a/surfsense_web/components/documents/FolderNode.tsx +++ b/surfsense_web/components/documents/FolderNode.tsx @@ -58,13 +58,20 @@ interface FolderNodeProps { onDelete: (folder: FolderDisplay) => void; onMove: (folder: FolderDisplay) => void; onCreateSubfolder: (parentId: number) => void; - onDropIntoFolder?: (itemType: "folder" | "document", itemId: number, targetFolderId: number) => void; + onDropIntoFolder?: ( + itemType: "folder" | "document", + itemId: number, + targetFolderId: number + ) => void; onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void; siblingPositions?: { before: string | null; after: string | null }; disabledDropIds?: Set; } -function getDropZone(monitor: { getClientOffset: () => { y: number } | null }, element: HTMLElement): DropZone { +function getDropZone( + monitor: { getClientOffset: () => { y: number } | null }, + element: HTMLElement +): DropZone { const offset = monitor.getClientOffset(); if (!offset) return "middle"; const rect = element.getBoundingClientRect(); @@ -104,7 +111,7 @@ export const FolderNode = React.memo(function FolderNode({ item: { id: folder.id, position: folder.position, parentId: folder.parentId }, collect: (monitor) => ({ isDragging: monitor.isDragging() }), }), - [folder.id, folder.position, folder.parentId], + [folder.id, folder.position, folder.parentId] ); const [{ isOver, canDrop }, drop] = useDrop( @@ -147,7 +154,14 @@ export const FolderNode = React.memo(function FolderNode({ canDrop: monitor.canDrop(), }), }), - [folder.id, folder.position, disabledDropIds, onDropIntoFolder, onReorderFolder, siblingPositions], + [ + folder.id, + folder.position, + disabledDropIds, + onDropIntoFolder, + onReorderFolder, + siblingPositions, + ] ); useEffect(() => { @@ -159,7 +173,7 @@ export const FolderNode = React.memo(function FolderNode({ rowRef.current = node; drag(drop(node)); }, - [drag, drop], + [drag, drop] ); useEffect(() => { @@ -188,7 +202,7 @@ export const FolderNode = React.memo(function FolderNode({ onCancelRename(); } }, - [handleRenameSubmit, folder.name, onCancelRename], + [handleRenameSubmit, folder.name, onCancelRename] ); const startRename = useCallback(() => { @@ -201,8 +215,11 @@ export const FolderNode = React.memo(function FolderNode({ return ( + {/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
onToggleExpand(folder.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onToggleExpand(folder.id); + } + }} onDoubleClick={(e) => { e.stopPropagation(); startRename(); @@ -322,7 +345,10 @@ export const FolderNode = React.memo(function FolderNode({ Move to... - onDelete(folder)}> + onDelete(folder)} + > Delete diff --git a/surfsense_web/components/documents/FolderPickerDialog.tsx b/surfsense_web/components/documents/FolderPickerDialog.tsx index 26c7c6442..366db1eb9 100644 --- a/surfsense_web/components/documents/FolderPickerDialog.tsx +++ b/surfsense_web/components/documents/FolderPickerDialog.tsx @@ -47,7 +47,8 @@ export function FolderPickerDialog({ const map: Record = {}; for (const f of folders) { const key = f.parentId ?? "root"; - (map[key] ??= []).push(f); + if (!map[key]) map[key] = []; + map[key].push(f); } return map; }, [folders]); @@ -88,7 +89,7 @@ export function FolderPickerDialog({ "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors", isSelected && "bg-accent text-accent-foreground", !isSelected && !isDisabled && "hover:bg-accent/50", - isDisabled && "cursor-not-allowed opacity-40", + isDisabled && "cursor-not-allowed opacity-40" )} style={{ paddingLeft: `${depth * 16 + 8}px` }} onClick={() => { @@ -96,7 +97,8 @@ export function FolderPickerDialog({ }} > {hasChildren ? ( - { e.stopPropagation(); @@ -108,7 +110,7 @@ export function FolderPickerDialog({ ) : ( )} - + ) : ( )} @@ -134,7 +136,7 @@ export function FolderPickerDialog({ className={cn( "flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors", selectedId === null && "bg-accent text-accent-foreground", - selectedId !== null && "hover:bg-accent/50", + selectedId !== null && "hover:bg-accent/50" )} onClick={() => setSelectedId(null)} > diff --git a/surfsense_web/components/documents/FolderTreeView.tsx b/surfsense_web/components/documents/FolderTreeView.tsx index 9d952433e..ca64ab1e0 100644 --- a/surfsense_web/components/documents/FolderTreeView.tsx +++ b/surfsense_web/components/documents/FolderTreeView.tsx @@ -8,7 +8,7 @@ import { HTML5Backend } from "react-dnd-html5-backend"; import { renamingFolderIdAtom } from "@/atoms/documents/folder.atoms"; import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import { DocumentNode, type DocumentNodeDoc } from "./DocumentNode"; -import { FolderNode, type FolderDisplay } from "./FolderNode"; +import { type FolderDisplay, FolderNode } from "./FolderNode"; interface FolderTreeViewProps { folders: FolderDisplay[]; @@ -16,7 +16,10 @@ interface FolderTreeViewProps { expandedIds: Set; onToggleExpand: (folderId: number) => void; mentionedDocIds: Set; - onToggleChatMention: (doc: { id: number; title: string; document_type: string }, isMentioned: boolean) => void; + onToggleChatMention: ( + doc: { id: number; title: string; document_type: string }, + isMentioned: boolean + ) => void; onRenameFolder: (folder: FolderDisplay, newName: string) => void; onDeleteFolder: (folder: FolderDisplay) => void; onMoveFolder: (folder: FolderDisplay) => void; @@ -26,7 +29,11 @@ interface FolderTreeViewProps { onDeleteDocument: (doc: DocumentNodeDoc) => void; onMoveDocument: (doc: DocumentNodeDoc) => void; activeTypes: DocumentTypeEnum[]; - onDropIntoFolder?: (itemType: "folder" | "document", itemId: number, targetFolderId: number | null) => void; + onDropIntoFolder?: ( + itemType: "folder" | "document", + itemId: number, + targetFolderId: number | null + ) => void; onReorderFolder?: (folderId: number, beforePos: string | null, afterPos: string | null) => void; } @@ -34,7 +41,8 @@ function groupBy(items: T[], keyFn: (item: T) => string | number): Record = {}; for (const item of items) { const key = keyFn(item); - (result[key] ??= []).push(item); + if (!result[key]) result[key] = []; + result[key].push(item); } return result; } @@ -58,15 +66,9 @@ export function FolderTreeView({ onDropIntoFolder, onReorderFolder, }: FolderTreeViewProps) { - const foldersByParent = useMemo( - () => groupBy(folders, (f) => f.parentId ?? "root"), - [folders], - ); + const foldersByParent = useMemo(() => groupBy(folders, (f) => f.parentId ?? "root"), [folders]); - const docsByFolder = useMemo( - () => groupBy(documents, (d) => d.folderId ?? "root"), - [documents], - ); + const docsByFolder = useMemo(() => groupBy(documents, (d) => d.folderId ?? "root"), [documents]); const folderChildCounts = useMemo(() => { const counts: Record = {}; @@ -82,12 +84,9 @@ export function FolderTreeView({ const [renamingFolderId, setRenamingFolderId] = useAtom(renamingFolderIdAtom); const handleStartRename = useCallback( (folderId: number) => setRenamingFolderId(folderId), - [setRenamingFolderId], - ); - const handleCancelRename = useCallback( - () => setRenamingFolderId(null), - [setRenamingFolderId], + [setRenamingFolderId] ); + const handleCancelRename = useCallback(() => setRenamingFolderId(null), [setRenamingFolderId]); const hasDescendantMatch = useMemo(() => { if (activeTypes.length === 0) return null; @@ -96,7 +95,7 @@ export function FolderTreeView({ function check(folderId: number): boolean { if (match[folderId] !== undefined) return match[folderId]; const childDocs = (docsByFolder[folderId] ?? []).some((d) => - activeTypes.includes(d.document_type as DocumentTypeEnum), + activeTypes.includes(d.document_type as DocumentTypeEnum) ); if (childDocs) { match[folderId] = true; @@ -127,10 +126,9 @@ export function FolderTreeView({ const visibleFolders = hasDescendantMatch ? childFolders.filter((f) => hasDescendantMatch[f.id]) : childFolders; - const childDocs = (docsByFolder[key] ?? []) - .filter( - (d) => activeTypes.length === 0 || activeTypes.includes(d.document_type as DocumentTypeEnum), - ); + const childDocs = (docsByFolder[key] ?? []).filter( + (d) => activeTypes.length === 0 || activeTypes.includes(d.document_type as DocumentTypeEnum) + ); const nodes: React.ReactNode[] = []; @@ -159,7 +157,7 @@ export function FolderTreeView({ onDropIntoFolder={onDropIntoFolder} onReorderFolder={onReorderFolder} siblingPositions={siblingPositions} - />, + /> ); if (expandedIds.has(f.id)) { @@ -179,7 +177,7 @@ export function FolderTreeView({ onEdit={onEditDocument} onDelete={onDeleteDocument} onMove={onMoveDocument} - />, + /> ); } @@ -208,9 +206,7 @@ export function FolderTreeView({ return ( -
- {treeNodes} -
+
{treeNodes}
); } diff --git a/surfsense_web/components/homepage/github-stars-badge.tsx b/surfsense_web/components/homepage/github-stars-badge.tsx index feee8ee33..da449635e 100644 --- a/surfsense_web/components/homepage/github-stars-badge.tsx +++ b/surfsense_web/components/homepage/github-stars-badge.tsx @@ -3,8 +3,8 @@ import { IconBrandGithub } from "@tabler/icons-react"; import { motion, useMotionValue, useSpring } from "motion/react"; import * as React from "react"; -import { cn } from "@/lib/utils"; import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Per-digit scrolling wheel diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index 3e7bccccb..299cf1032 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -35,7 +35,14 @@ const HeroCarousel = dynamic( // Official Google "G" logo with brand colors const GoogleLogo = ({ className }: { className?: string }) => ( - + + Google logo + {/* biome-ignore lint/a11y/useSemanticElements: div wraps img, button would break layout */}
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + open(); + } + }} > )} - {/* Main content panel */} - - {children} - + {/* Main content panel */} + + {children} + {/* Right panel — tabbed Sources/Report (desktop only) */} {documentsPanel && ( diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx index 2d6860b1f..a433b4e3c 100644 --- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx @@ -1,7 +1,7 @@ "use client"; +import { useQuery } from "@rocicorp/zero/react"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { openDocumentTabAtom } from "@/atoms/tabs/tabs.atom"; import { ChevronLeft, ChevronRight, Unplug } from "lucide-react"; import { useParams } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -17,22 +17,22 @@ import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dial import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; import { expandedFolderIdsAtom } from "@/atoms/documents/folder.atoms"; +import { openDocumentTabAtom } from "@/atoms/tabs/tabs.atom"; import { CreateFolderDialog } from "@/components/documents/CreateFolderDialog"; import type { DocumentNodeDoc } from "@/components/documents/DocumentNode"; -import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog"; import type { FolderDisplay } from "@/components/documents/FolderNode"; +import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog"; import { FolderTreeView } from "@/components/documents/FolderTreeView"; import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { DocumentTypeEnum } from "@/contracts/types/document.types"; -import { foldersApiService } from "@/lib/apis/folders-api.service"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useDocumentSearch } from "@/hooks/use-document-search"; import { useDocuments } from "@/hooks/use-documents"; import { useMediaQuery } from "@/hooks/use-media-query"; -import { useQuery } from "@rocicorp/zero/react"; +import { foldersApiService } from "@/lib/apis/folders-api.service"; import { queries } from "@/zero/queries/index"; import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; @@ -91,7 +91,7 @@ export function DocumentsSidebar({ const [expandedFolderMap, setExpandedFolderMap] = useAtom(expandedFolderIdsAtom); const expandedIds = useMemo( () => new Set(expandedFolderMap[searchSpaceId] ?? []), - [expandedFolderMap, searchSpaceId], + [expandedFolderMap, searchSpaceId] ); const toggleFolderExpand = useCallback( (folderId: number) => { @@ -102,7 +102,7 @@ export function DocumentsSidebar({ return { ...prev, [searchSpaceId]: [...current] }; }); }, - [searchSpaceId, setExpandedFolderMap], + [searchSpaceId, setExpandedFolderMap] ); // Zero queries for tree data @@ -118,7 +118,7 @@ export function DocumentsSidebar({ parentId: f.parentId ?? null, searchSpaceId: f.searchSpaceId, })), - [zeroFolders], + [zeroFolders] ); const treeDocuments: DocumentNodeDoc[] = useMemo( @@ -132,14 +132,15 @@ export function DocumentsSidebar({ folderId: (d as { folderId?: number | null }).folderId ?? null, status: d.status as { state: string; reason?: string | null } | undefined, })), - [zeroAllDocs], + [zeroAllDocs] ); const foldersByParent = useMemo(() => { const map: Record = {}; for (const f of treeFolders) { const key = String(f.parentId ?? "root"); - (map[key] ??= []).push(f); + if (!map[key]) map[key] = []; + map[key].push(f); } return map; }, [treeFolders]); @@ -161,13 +162,10 @@ export function DocumentsSidebar({ return treeFolders.find((f) => f.id === createFolderParentId)?.name ?? null; }, [createFolderParentId, treeFolders]); - const handleCreateFolder = useCallback( - (parentId: number | null) => { - setCreateFolderParentId(parentId); - setCreateFolderOpen(true); - }, - [], - ); + const handleCreateFolder = useCallback((parentId: number | null) => { + setCreateFolderParentId(parentId); + setCreateFolderOpen(true); + }, []); const handleCreateFolderConfirm = useCallback( async (name: string) => { @@ -185,37 +183,31 @@ export function DocumentsSidebar({ return { ...prev, [searchSpaceId]: [...current] }; }); } - } catch (e: any) { - toast.error(e?.message || "Failed to create folder"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to create folder"); } }, - [createFolderParentId, searchSpaceId, setExpandedFolderMap], + [createFolderParentId, searchSpaceId, setExpandedFolderMap] ); - const handleRenameFolder = useCallback( - async (folder: FolderDisplay, newName: string) => { - try { - await foldersApiService.updateFolder(folder.id, { name: newName }); - toast.success("Folder renamed"); - } catch (e: any) { - toast.error(e?.message || "Failed to rename folder"); - } - }, - [], - ); + const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => { + try { + await foldersApiService.updateFolder(folder.id, { name: newName }); + toast.success("Folder renamed"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to rename folder"); + } + }, []); - const handleDeleteFolder = useCallback( - async (folder: FolderDisplay) => { - if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; - try { - await foldersApiService.deleteFolder(folder.id); - toast.success("Folder deleted"); - } catch (e: any) { - toast.error(e?.message || "Failed to delete folder"); - } - }, - [], - ); + const handleDeleteFolder = useCallback(async (folder: FolderDisplay) => { + if (!confirm(`Delete folder "${folder.name}" and all its contents?`)) return; + try { + await foldersApiService.deleteFolder(folder.id); + toast.success("Folder deleted"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to delete folder"); + } + }, []); const handleMoveFolder = useCallback( (folder: FolderDisplay) => { @@ -234,7 +226,7 @@ export function DocumentsSidebar({ }); setFolderPickerOpen(true); }, - [foldersByParent], + [foldersByParent] ); const handleMoveDocument = useCallback((doc: DocumentNodeDoc) => { @@ -257,12 +249,12 @@ export function DocumentsSidebar({ }); toast.success("Document moved"); } - } catch (e: any) { - toast.error(e?.message || "Failed to move item"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to move item"); } setFolderPickerTarget(null); }, - [folderPickerTarget], + [folderPickerTarget] ); const handleDropIntoFolder = useCallback( @@ -279,11 +271,11 @@ export function DocumentsSidebar({ }); toast.success("Document moved"); } - } catch (e: any) { - toast.error(e?.message || "Failed to move item"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to move item"); } }, - [], + [] ); const handleReorderFolder = useCallback( @@ -293,11 +285,11 @@ export function DocumentsSidebar({ before_position: beforePos, after_position: afterPos, }); - } catch (e: any) { - toast.error(e?.message || "Failed to reorder folder"); + } catch (e: unknown) { + toast.error((e as Error)?.message || "Failed to reorder folder"); } }, - [], + [] ); const handleToggleChatMention = useCallback( @@ -598,20 +590,20 @@ export function DocumentsSidebar({ onDeleteFolder={handleDeleteFolder} onMoveFolder={handleMoveFolder} onCreateFolder={handleCreateFolder} - onPreviewDocument={(doc) => { - openDocumentTab({ - documentId: doc.id, - searchSpaceId, - title: doc.title, - }); - }} - onEditDocument={(doc) => { - openDocumentTab({ - documentId: doc.id, - searchSpaceId, - title: doc.title, - }); - }} + onPreviewDocument={(doc) => { + openDocumentTab({ + documentId: doc.id, + searchSpaceId, + title: doc.title, + }); + }} + onEditDocument={(doc) => { + openDocumentTab({ + documentId: doc.id, + searchSpaceId, + title: doc.title, + }); + }} onDeleteDocument={(doc) => handleDeleteDocument(doc.id)} onMoveDocument={handleMoveDocument} activeTypes={activeTypes} @@ -625,11 +617,7 @@ export function DocumentsSidebar({ open={folderPickerOpen} onOpenChange={setFolderPickerOpen} folders={treeFolders} - title={ - folderPickerTarget?.type === "folder" - ? "Move folder to..." - : "Move document to..." - } + title={folderPickerTarget?.type === "folder" ? "Move folder to..." : "Move document to..."} description="Select a destination folder, or choose Root to move to the top level." disabledFolderIds={folderPickerTarget?.disabledIds} onSelect={handleFolderPickerSelect} diff --git a/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx b/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx index e500590ed..7cec16bfa 100644 --- a/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx +++ b/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx @@ -176,9 +176,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
-

- {doc.title || title || "Untitled"} -

+

{doc.title || title || "Untitled"}

{editedMarkdown !== null && (

Unsaved changes

)} @@ -221,7 +219,12 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen {doc.title || title || "Untitled"} {doc.document_type === "NOTE" && ( - diff --git a/surfsense_web/components/layout/ui/tabs/TabBar.tsx b/surfsense_web/components/layout/ui/tabs/TabBar.tsx index 3bc9cb023..14de3add6 100644 --- a/surfsense_web/components/layout/ui/tabs/TabBar.tsx +++ b/surfsense_web/components/layout/ui/tabs/TabBar.tsx @@ -2,13 +2,13 @@ import { useAtomValue, useSetAtom } from "jotai"; import { FileText, MessageSquare, Plus, X } from "lucide-react"; -import { useCallback, useRef, useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { activeTabIdAtom, closeTabAtom, switchTabAtom, - tabsAtom, type Tab, + tabsAtom, } from "@/atoms/tabs/tabs.atom"; import { cn } from "@/lib/utils"; @@ -58,16 +58,8 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) { if (tabs.length <= 1) return null; return ( -
-
+
+
{tabs.map((tab) => { const isActive = tab.id === activeTabId; const Icon = tab.type === "document" ? FileText : MessageSquare; @@ -85,11 +77,10 @@ export function TabBar({ onTabSwitch, onNewChat, className }: TabBarProps) { : "bg-muted/30 text-muted-foreground hover:bg-muted/60 hover:text-foreground" )} > - {isActive && ( - - )} + {isActive && } {tab.title} + {/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */} searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), - enabled: !!searchSpaceId, - }); +export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManagerProps) { + const t = useTranslations("searchSpaceSettings"); + const tCommon = useTranslations("common"); + const { + data: searchSpace, + isLoading: loading, + refetch: fetchSearchSpace, + } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), + enabled: !!searchSpaceId, + }); - const { mutateAsync: updateSearchSpace } = useAtomValue( - updateSearchSpaceMutationAtom, - ); + const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [saving, setSaving] = useState(false); - const [hasChanges, setHasChanges] = useState(false); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); - // Initialize state from fetched search space - useEffect(() => { - if (searchSpace) { - setName(searchSpace.name || ""); - setDescription(searchSpace.description || ""); - setHasChanges(false); - } - }, [searchSpace]); + // Initialize state from fetched search space + useEffect(() => { + if (searchSpace) { + setName(searchSpace.name || ""); + setDescription(searchSpace.description || ""); + setHasChanges(false); + } + }, [searchSpace]); - // Track changes - useEffect(() => { - if (searchSpace) { - const currentName = searchSpace.name || ""; - const currentDescription = searchSpace.description || ""; - const changed = - currentName !== name || currentDescription !== description; - setHasChanges(changed); - } - }, [searchSpace, name, description]); + // Track changes + useEffect(() => { + if (searchSpace) { + const currentName = searchSpace.name || ""; + const currentDescription = searchSpace.description || ""; + const changed = currentName !== name || currentDescription !== description; + setHasChanges(changed); + } + }, [searchSpace, name, description]); - const handleSave = async () => { - try { - setSaving(true); + const handleSave = async () => { + try { + setSaving(true); - await updateSearchSpace({ - id: searchSpaceId, - data: { - name: name.trim(), - description: description.trim() || undefined, - }, - }); + await updateSearchSpace({ + id: searchSpaceId, + data: { + name: name.trim(), + description: description.trim() || undefined, + }, + }); - setHasChanges(false); - await fetchSearchSpace(); - } catch (error: any) { - console.error("Error saving search space details:", error); - toast.error(error.message || "Failed to save search space details"); - } finally { - setSaving(false); - } - }; + setHasChanges(false); + await fetchSearchSpace(); + } catch (error: any) { + console.error("Error saving search space details:", error); + toast.error(error.message || "Failed to save search space details"); + } finally { + setSaving(false); + } + }; - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSave(); - }; + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSave(); + }; - if (loading) { - return ( -
- - - - - - - - - - -
- ); - } + if (loading) { + return ( +
+ + + + + + + + + + +
+ ); + } - return ( -
- - - - Update your search space name and description. These details help - identify and organize your workspace. - - + return ( +
+ + + + Update your search space name and description. These details help identify and organize + your workspace. + + - {/* Search Space Details Card */} -
- - - - Search Space Details - - - Manage the basic information for this search space. - - - -
- - setName(e.target.value)} - className="text-sm md:text-base h-9 md:h-10" - /> -

- {t("general_name_description")} -

-
+ {/* Search Space Details Card */} + + + + Search Space Details + + Manage the basic information for this search space. + + + +
+ + setName(e.target.value)} + className="text-sm md:text-base h-9 md:h-10" + /> +

+ {t("general_name_description")} +

+
-
- - setDescription(e.target.value)} - className="text-sm md:text-base h-9 md:h-10" - /> -

- {t("general_description_description")} -

-
-
-
+
+ + setDescription(e.target.value)} + className="text-sm md:text-base h-9 md:h-10" + /> +

+ {t("general_description_description")} +

+
+
+
- {/* Action Buttons */} -
- -
-
-
- ); + {/* Action Buttons */} +
+ +
+ +
+ ); } diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index dc3a15a7d..b3cd64a5b 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -6,13 +6,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { Textarea } from "@/components/ui/textarea"; @@ -22,197 +16,187 @@ import { cacheKeys } from "@/lib/query-client/cache-keys"; import { Spinner } from "../ui/spinner"; interface PromptConfigManagerProps { - searchSpaceId: number; + searchSpaceId: number; } -export function PromptConfigManager({ - searchSpaceId, -}: PromptConfigManagerProps) { - const { - data: searchSpace, - isLoading: loading, - refetch: fetchSearchSpace, - } = useQuery({ - queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), - queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), - enabled: !!searchSpaceId, - }); +export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) { + const { + data: searchSpace, + isLoading: loading, + refetch: fetchSearchSpace, + } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), + enabled: !!searchSpaceId, + }); - const [customInstructions, setCustomInstructions] = useState(""); - const [saving, setSaving] = useState(false); - const [hasChanges, setHasChanges] = useState(false); + const [customInstructions, setCustomInstructions] = useState(""); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); - // Initialize state from fetched search space - useEffect(() => { - if (searchSpace) { - setCustomInstructions(searchSpace.qna_custom_instructions || ""); - setHasChanges(false); - } - }, [searchSpace]); + // Initialize state from fetched search space + useEffect(() => { + if (searchSpace) { + setCustomInstructions(searchSpace.qna_custom_instructions || ""); + setHasChanges(false); + } + }, [searchSpace]); - // Track changes - useEffect(() => { - if (searchSpace) { - const currentCustom = searchSpace.qna_custom_instructions || ""; - const changed = currentCustom !== customInstructions; - setHasChanges(changed); - } - }, [searchSpace, customInstructions]); + // Track changes + useEffect(() => { + if (searchSpace) { + const currentCustom = searchSpace.qna_custom_instructions || ""; + const changed = currentCustom !== customInstructions; + setHasChanges(changed); + } + }, [searchSpace, customInstructions]); - const handleSave = async () => { - try { - setSaving(true); + const handleSave = async () => { + try { + setSaving(true); - const payload = { - qna_custom_instructions: customInstructions.trim() || "", - }; + const payload = { + qna_custom_instructions: customInstructions.trim() || "", + }; - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, - { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }, - ); + const response = await authenticatedFetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + } + ); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.detail || "Failed to save system instructions", - ); - } + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to save system instructions"); + } - toast.success("System instructions saved successfully"); - setHasChanges(false); - await fetchSearchSpace(); - } catch (error: any) { - console.error("Error saving system instructions:", error); - toast.error(error.message || "Failed to save system instructions"); - } finally { - setSaving(false); - } - }; + toast.success("System instructions saved successfully"); + setHasChanges(false); + await fetchSearchSpace(); + } catch (error: any) { + console.error("Error saving system instructions:", error); + toast.error(error.message || "Failed to save system instructions"); + } finally { + setSaving(false); + } + }; - const onSubmit = (e: React.FormEvent) => { - e.preventDefault(); - handleSave(); - }; + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSave(); + }; - if (loading) { - return ( -
- - - - - - - - - - -
- ); - } + if (loading) { + return ( +
+ + + + + + + + + + +
+ ); + } - return ( -
- {/* Work in Progress Notice */} - - - - Work in Progress: This - functionality is currently under development and not yet connected to - the backend. Your instructions will be saved but won't affect AI - behavior until the feature is fully implemented. - - + return ( +
+ {/* Work in Progress Notice */} + + + + Work in Progress: This functionality is currently + under development and not yet connected to the backend. Your instructions will be saved + but won't affect AI behavior until the feature is fully implemented. + + - - - - System instructions apply to all AI interactions in this search space. - They guide how the AI responds, its tone, focus areas, and behavior - patterns. - - + + + + System instructions apply to all AI interactions in this search space. They guide how the + AI responds, its tone, focus areas, and behavior patterns. + + - {/* System Instructions Card */} -
- - - - Custom System Instructions - - - Provide specific guidelines for how you want the AI to respond. - These instructions will be applied to all answers in this search - space. - - - -
- -