chore: linting

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-03-27 03:17:05 -07:00
parent 23b4f91754
commit 64c913baa3
47 changed files with 908 additions and 895 deletions

View file

@ -4,133 +4,130 @@ import { motion } from "motion/react";
import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full px-6 py-4 space-y-6 min-h-[calc(100vh-64px)]"
>
{/* Summary Dashboard Skeleton */}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{[...Array(4)].map((_, i) => (
<div key={i} className="rounded-lg border p-4">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-32" />
</div>
</div>
))}
</motion.div>
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="w-full px-6 py-4 space-y-6 min-h-[calc(100vh-64px)]"
>
{/* Summary Dashboard Skeleton */}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
{[...Array(4)].map((_, i) => (
<div key={i} className="rounded-lg border p-4">
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-4 rounded-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-3 w-32" />
</div>
</div>
))}
</motion.div>
{/* Header Section Skeleton */}
<motion.div
className="flex items-center justify-between"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64" />
</div>
<Skeleton className="h-9 w-24" />
</motion.div>
{/* Header Section Skeleton */}
<motion.div
className="flex items-center justify-between"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="space-y-2">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-4 w-64" />
</div>
<Skeleton className="h-9 w-24" />
</motion.div>
{/* Filters Skeleton */}
<motion.div
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
<Skeleton className="h-9 w-full sm:w-60" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-20" />
</div>
</motion.div>
{/* Filters Skeleton */}
<motion.div
className="flex flex-wrap items-center justify-start gap-3 w-full"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center gap-3 flex-wrap w-full sm:w-auto">
<Skeleton className="h-9 w-full sm:w-60" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-20" />
</div>
</motion.div>
{/* Table Skeleton */}
<motion.div
className="rounded-md border overflow-hidden"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
{/* Table Header */}
<div className="border-b bg-muted/50 px-4 py-3 flex items-center gap-4">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-8" />
</div>
{/* Table Skeleton */}
<motion.div
className="rounded-md border overflow-hidden"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
{/* Table Header */}
<div className="border-b bg-muted/50 px-4 py-3 flex items-center gap-4">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-8" />
</div>
{/* Table Rows */}
{[...Array(6)].map((_, i) => (
<div
key={i}
className="border-b px-4 py-3 flex items-center gap-4 hover:bg-muted/50"
>
<Skeleton className="h-4 w-4" />
<Skeleton className="h-6 w-12 rounded-full" />
<Skeleton className="h-6 w-16 rounded-full" />
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-20" />
</div>
<div className="flex-1 space-y-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
<div className="space-y-1">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-3 w-20" />
</div>
<Skeleton className="h-8 w-8" />
</div>
))}
</motion.div>
{/* Table Rows */}
{[...Array(6)].map((_, i) => (
<div key={i} className="border-b px-4 py-3 flex items-center gap-4 hover:bg-muted/50">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-6 w-12 rounded-full" />
<Skeleton className="h-6 w-16 rounded-full" />
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-20" />
</div>
<div className="flex-1 space-y-1">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-48" />
</div>
<div className="space-y-1">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-3 w-20" />
</div>
<Skeleton className="h-8 w-8" />
</div>
))}
</motion.div>
{/* Pagination Skeleton */}
<div className="flex items-center justify-between gap-8 mt-4">
<motion.div
className="flex items-center gap-3"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
>
<Skeleton className="h-4 w-20 max-sm:sr-only" />
<Skeleton className="h-9 w-16" />
</motion.div>
{/* Pagination Skeleton */}
<div className="flex items-center justify-between gap-8 mt-4">
<motion.div
className="flex items-center gap-3"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
>
<Skeleton className="h-4 w-20 max-sm:sr-only" />
<Skeleton className="h-9 w-16" />
</motion.div>
<motion.div
className="flex grow justify-end"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
<Skeleton className="h-4 w-40" />
</motion.div>
<motion.div
className="flex grow justify-end"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
<Skeleton className="h-4 w-40" />
</motion.div>
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
</div>
</div>
</motion.div>
);
<div className="flex items-center gap-2">
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
<Skeleton className="h-9 w-9" />
</div>
</div>
</motion.div>
);
}

View file

@ -1,10 +1,10 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() {
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-4">
<Skeleton className="h-4 w-64" />
<Skeleton className="h-32 w-full max-w-2xl rounded-xl" />
</div>
);
return (
<div className="flex flex-1 flex-col items-center justify-center gap-4 p-4">
<Skeleton className="h-4 w-64" />
<Skeleton className="h-32 w-full max-w-2xl rounded-xl" />
</div>
);
}

View file

@ -1527,9 +1527,7 @@ export default function NewChatPage() {
// Show loading state only when loading an existing thread
if (isInitializing) {
return (
<Loading />
);
return <Loading />;
}
// Show error state only if we tried to load an existing thread but failed
@ -1565,4 +1563,4 @@ export default function NewChatPage() {
</div>
</AssistantRuntimeProvider>
);
}
}

View file

@ -1,45 +1,45 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function Loading() {
return (
<div className="flex h-[calc(100dvh-64px)] flex-col bg-main-panel px-4">
<div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8">
{/* User message */}
<div className="flex justify-end">
<Skeleton className="h-12 w-56 rounded-2xl" />
</div>
return (
<div className="flex h-[calc(100dvh-64px)] flex-col bg-main-panel px-4">
<div className="mx-auto w-full max-w-[44rem] flex flex-1 flex-col gap-6 py-8">
{/* User message */}
<div className="flex justify-end">
<Skeleton className="h-12 w-56 rounded-2xl" />
</div>
{/* Assistant message */}
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[85%]" />
<Skeleton className="h-18 w-[40%]" />
</div>
{/* Assistant message */}
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[85%]" />
<Skeleton className="h-18 w-[40%]" />
</div>
{/* User message */}
<div className="flex gap-2 justify-end">
<Skeleton className="h-12 w-72 rounded-2xl" />
</div>
{/* User message */}
<div className="flex gap-2 justify-end">
<Skeleton className="h-12 w-72 rounded-2xl" />
</div>
{/* Assistant message */}
<div className="flex flex-col gap-2">
<Skeleton className="h-10 w-[30%]" />
<Skeleton className="h-4 w-[90%]" />
<Skeleton className="h-6 w-[60%]" />
</div>
{/* Assistant message */}
<div className="flex flex-col gap-2">
<Skeleton className="h-10 w-[30%]" />
<Skeleton className="h-4 w-[90%]" />
<Skeleton className="h-6 w-[60%]" />
</div>
{/* User message */}
<div className="flex gap-2 justify-end">
<Skeleton className="h-12 w-96 rounded-2xl" />
</div>
</div>
{/* User message */}
<div className="flex gap-2 justify-end">
<Skeleton className="h-12 w-96 rounded-2xl" />
</div>
</div>
{/* Input bar */}
<div className="sticky bottom-0 pb-6 bg-main-panel">
<div className="mx-auto w-full max-w-[44rem]">
<Skeleton className="h-24 w-full rounded-2xl" />
</div>
</div>
</div>
);
{/* Input bar */}
<div className="sticky bottom-0 pb-6 bg-main-panel">
<div className="mx-auto w-full max-w-[44rem]">
<Skeleton className="h-24 w-full rounded-2xl" />
</div>
</div>
</div>
);
}

View file

@ -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`);
}

View file

@ -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(() => {

View file

@ -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 (
<html lang="en">
<body>
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
<h2 className="text-xl font-semibold">Something went wrong</h2>
<p className="text-sm text-muted-foreground">
An unexpected error occurred.
</p>
<Button onClick={reset}>Try again</Button>
</div>
</body>
</html>
);
return (
<html lang="en">
<body>
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-4">
<h2 className="text-xl font-semibold">Something went wrong</h2>
<p className="text-sm text-muted-foreground">An unexpected error occurred.</p>
<Button onClick={reset}>Try again</Button>
</div>
</body>
</html>
);
}

View file

@ -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 <PublicChatView shareToken={token} />;
return <PublicChatView shareToken={token} />;
}

View file

@ -9,7 +9,7 @@ import { atomWithStorage } from "jotai/utils";
*/
export const expandedFolderIdsAtom = atomWithStorage<Record<number, number[]>>(
"surfsense:expandedFolderIds",
{},
{}
);
/**

View file

@ -41,7 +41,7 @@ export const tabsStateAtom = atomWithStorage<TabsState>(
"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;
}

View file

@ -28,16 +28,14 @@ export const InlineCitation: FC<InlineCitationProps> = ({ chunkId, isDocsChunk =
url=""
isDocsChunk={isDocsChunk}
>
<span
<button
type="button"
onClick={() => 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}
</span>
</button>
</SourceDetailPanel>
);
};

View file

@ -225,17 +225,13 @@ function ThreadListItemComponent({
onDelete,
}: ThreadListItemComponentProps) {
return (
<div
<button
type="button"
className={cn(
"group flex items-center gap-2 rounded-lg px-3 py-2 transition-colors cursor-pointer",
"group flex w-full items-center gap-2 rounded-lg px-3 py-2 transition-colors cursor-pointer text-left",
isActive ? "bg-accent text-accent-foreground" : "hover:bg-muted/50"
)}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") onClick();
}}
role="button"
tabIndex={0}
>
<MessageSquareIcon className="size-4 shrink-0 text-muted-foreground" />
<div className="flex-1 min-w-0">
@ -274,7 +270,7 @@ function ThreadListItemComponent({
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</button>
);
}

View file

@ -8,7 +8,14 @@ import { cn } from "@/lib/utils";
// Official Google "G" logo with brand colors
const GoogleLogo = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Google logo"
>
<title>Google logo</title>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"

View file

@ -185,7 +185,20 @@ export const Grid = ({ pattern, size }: { pattern?: [number, number][]; size?: n
);
};
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][] }) {
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 (

View file

@ -45,7 +45,7 @@ export function CreateFolderDialog({
onConfirm(trimmed);
onOpenChange(false);
},
[name, onConfirm, onOpenChange],
[name, onConfirm, onOpenChange]
);
const isSubfolder = !!parentFolderName;

View file

@ -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 (
<ContextMenu>
<ContextMenuTrigger asChild>
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag ref */}
<div
ref={drag}
role="button"
tabIndex={0}
className={cn(
"group flex h-8 items-center gap-1.5 rounded-md px-1 text-sm hover:bg-accent/50 cursor-pointer select-none",
isMentioned && "bg-accent/30",
isDragging && "opacity-40",
isDragging && "opacity-40"
)}
style={{ paddingLeft: `${depth * 16 + 4}px` }}
onClick={handleCheckChange}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleCheckChange();
}
}}
>
{isSelectable ? (
<Checkbox
@ -110,7 +111,7 @@ export const DocumentNode = React.memo(function DocumentNode({
"h-2 w-2 rounded-full",
statusState === "processing" && "animate-pulse bg-amber-500",
statusState === "pending" && "bg-muted-foreground/40",
statusState === "failed" && "bg-destructive",
statusState === "failed" && "bg-destructive"
)}
/>
</span>
@ -119,7 +120,10 @@ export const DocumentNode = React.memo(function DocumentNode({
<span className="flex-1 min-w-0 truncate">{doc.title}</span>
<span className="shrink-0">
{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"
)}
</span>
<DropdownMenu>
@ -134,10 +138,10 @@ export const DocumentNode = React.memo(function DocumentNode({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem onClick={() => onPreview(doc)}>
<Eye className="mr-2 h-4 w-4" />
Open
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onPreview(doc)}>
<Eye className="mr-2 h-4 w-4" />
Open
</DropdownMenuItem>
{isEditable && (
<DropdownMenuItem onClick={() => onEdit(doc)}>
<Pencil className="mr-2 h-4 w-4" />
@ -163,10 +167,10 @@ export const DocumentNode = React.memo(function DocumentNode({
</ContextMenuTrigger>
<ContextMenuContent className="w-44">
<ContextMenuItem onClick={() => onPreview(doc)}>
<Eye className="mr-2 h-4 w-4" />
Open
</ContextMenuItem>
<ContextMenuItem onClick={() => onPreview(doc)}>
<Eye className="mr-2 h-4 w-4" />
Open
</ContextMenuItem>
{isEditable && (
<ContextMenuItem onClick={() => onEdit(doc)}>
<Pencil className="mr-2 h-4 w-4" />

View file

@ -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<number>;
}
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 (
<ContextMenu>
<ContextMenuTrigger asChild disabled={isRenaming}>
{/* biome-ignore lint/a11y/useSemanticElements: div required for drag/drop refs */}
<div
ref={attachRef}
role="button"
tabIndex={0}
className={cn(
"group relative flex h-8 items-center gap-1 rounded-md px-1 text-sm hover:bg-accent/50 cursor-pointer select-none",
isExpanded && "font-medium",
@ -210,10 +227,16 @@ export const FolderNode = React.memo(function FolderNode({
isOver && canDrop && dropZone === "middle" && "bg-accent ring-1 ring-primary/40",
isOver && canDrop && dropZone === "top" && "border-t-2 border-primary",
isOver && canDrop && dropZone === "bottom" && "border-b-2 border-primary",
isOver && !canDrop && "cursor-not-allowed",
isOver && !canDrop && "cursor-not-allowed"
)}
style={{ paddingLeft: `${depth * 16 + 4}px` }}
onClick={() => 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...
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem className="text-destructive focus:text-destructive" onClick={() => onDelete(folder)}>
<ContextMenuItem
className="text-destructive focus:text-destructive"
onClick={() => onDelete(folder)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</ContextMenuItem>

View file

@ -47,7 +47,8 @@ export function FolderPickerDialog({
const map: Record<string, FolderDisplay[]> = {};
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 ? (
<span
<button
type="button"
className="flex h-4 w-4 shrink-0 items-center justify-center"
onClick={(e) => {
e.stopPropagation();
@ -108,7 +110,7 @@ export function FolderPickerDialog({
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</span>
</button>
) : (
<span className="h-4 w-4 shrink-0" />
)}
@ -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)}
>

View file

@ -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<number>;
onToggleExpand: (folderId: number) => void;
mentionedDocIds: Set<number>;
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<T>(items: T[], keyFn: (item: T) => string | number): Record<str
const result: Record<string | number, T[]> = {};
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<number, number> = {};
@ -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 (
<DndProvider backend={HTML5Backend}>
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-1">
{treeNodes}
</div>
<div className="flex-1 min-h-0 overflow-y-auto px-2 py-1">{treeNodes}</div>
</DndProvider>
);
}

View file

@ -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

View file

@ -35,7 +35,14 @@ const HeroCarousel = dynamic(
// Official Google "G" logo with brand colors
const GoogleLogo = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<svg
className={className}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Google logo"
>
<title>Google logo</title>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"

View file

@ -63,9 +63,18 @@ function UseCaseCard({
transition={{ duration: 0.5, ease: "easeOut" }}
className={`group overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-sm transition-shadow duration-300 hover:shadow-xl dark:border-neutral-700/60 dark:bg-neutral-900 ${className ?? ""}`}
>
{/* biome-ignore lint/a11y/useSemanticElements: div wraps img, button would break layout */}
<div
role="button"
tabIndex={0}
className="cursor-pointer overflow-hidden bg-neutral-50 p-2 dark:bg-neutral-950"
onClick={open}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
open();
}
}}
>
<img
src={src}

View file

@ -14,13 +14,13 @@ import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { resetTabsAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import {
morePagesDialogAtom,
searchSpaceSettingsDialogAtom,
teamDialogAtom,
userSettingsDialogAtom,
} from "@/atoms/settings/settings-dialog.atoms";
import { resetTabsAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";

View file

@ -497,14 +497,10 @@ export function LayoutShell({
/>
)}
{/* Main content panel */}
<MainContentPanel
isChatPage={isChatPage}
onTabSwitch={onTabSwitch}
onNewChat={onNewChat}
>
{children}
</MainContentPanel>
{/* Main content panel */}
<MainContentPanel isChatPage={isChatPage} onTabSwitch={onTabSwitch} onNewChat={onNewChat}>
{children}
</MainContentPanel>
{/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel && (

View file

@ -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<string, FolderDisplay[]> = {};
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}

View file

@ -176,9 +176,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
<div className="flex flex-col h-full overflow-hidden">
<div className="flex items-center justify-between px-6 py-3 border-b shrink-0">
<div className="flex-1 min-w-0">
<h1 className="text-base font-semibold truncate">
{doc.title || title || "Untitled"}
</h1>
<h1 className="text-base font-semibold truncate">{doc.title || title || "Untitled"}</h1>
{editedMarkdown !== null && (
<p className="text-xs text-muted-foreground">Unsaved changes</p>
)}
@ -221,7 +219,12 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
{doc.title || title || "Untitled"}
</h1>
{doc.document_type === "NOTE" && (
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)} className="gap-1.5">
<Button
variant="outline"
size="sm"
onClick={() => setIsEditing(true)}
className="gap-1.5"
>
<Pencil className="size-3.5" />
Edit
</Button>

View file

@ -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 (
<div
className={cn(
"flex items-center shrink-0 border-b bg-main-panel",
className
)}
>
<div
ref={scrollRef}
className="flex items-center flex-1 overflow-x-auto scrollbar-none"
>
<div className={cn("flex items-center shrink-0 border-b bg-main-panel", className)}>
<div ref={scrollRef} className="flex items-center flex-1 overflow-x-auto scrollbar-none">
{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 && (
<span className="absolute bottom-0 left-0 right-0 h-[2px] bg-primary" />
)}
{isActive && <span className="absolute bottom-0 left-0 right-0 h-[2px] bg-primary" />}
<Icon className="size-3.5 shrink-0" />
<span className="truncate">{tab.title}</span>
{/* biome-ignore lint/a11y/useSemanticElements: cannot nest button inside button */}
<span
role="button"
tabIndex={0}

View file

@ -9,13 +9,7 @@ import { toast } from "sonner";
import { updateSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
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 { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
@ -24,175 +18,163 @@ import { cacheKeys } from "@/lib/query-client/cache-keys";
import { Spinner } from "../ui/spinner";
interface GeneralSettingsManagerProps {
searchSpaceId: number;
searchSpaceId: number;
}
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,
});
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 (
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<Skeleton className="h-10 md:h-12 w-full" />
<Skeleton className="h-10 md:h-12 w-full" />
</CardContent>
</Card>
</div>
);
}
if (loading) {
return (
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<Skeleton className="h-10 md:h-12 w-full" />
<Skeleton className="h-10 md:h-12 w-full" />
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-4 md:space-y-6">
<Alert className="bg-muted/50 py-3 md:py-4">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
Update your search space name and description. These details help
identify and organize your workspace.
</AlertDescription>
</Alert>
return (
<div className="space-y-4 md:space-y-6">
<Alert className="bg-muted/50 py-3 md:py-4">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
Update your search space name and description. These details help identify and organize
your workspace.
</AlertDescription>
</Alert>
{/* Search Space Details Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">
Search Space Details
</CardTitle>
<CardDescription className="text-xs md:text-sm">
Manage the basic information for this search space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 md:space-y-5 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="search-space-name"
className="text-sm md:text-base font-medium"
>
{t("general_name_label")}
</Label>
<Input
id="search-space-name"
placeholder={t("general_name_placeholder")}
value={name}
onChange={(e) => setName(e.target.value)}
className="text-sm md:text-base h-9 md:h-10"
/>
<p className="text-[10px] md:text-xs text-muted-foreground">
{t("general_name_description")}
</p>
</div>
{/* Search Space Details Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">Search Space Details</CardTitle>
<CardDescription className="text-xs md:text-sm">
Manage the basic information for this search space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 md:space-y-5 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label htmlFor="search-space-name" className="text-sm md:text-base font-medium">
{t("general_name_label")}
</Label>
<Input
id="search-space-name"
placeholder={t("general_name_placeholder")}
value={name}
onChange={(e) => setName(e.target.value)}
className="text-sm md:text-base h-9 md:h-10"
/>
<p className="text-[10px] md:text-xs text-muted-foreground">
{t("general_name_description")}
</p>
</div>
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="search-space-description"
className="text-sm md:text-base font-medium"
>
{t("general_description_label")}{" "}
<span className="text-muted-foreground font-normal">
({tCommon("optional")})
</span>
</Label>
<Input
id="search-space-description"
placeholder={t("general_description_placeholder")}
value={description}
onChange={(e) => setDescription(e.target.value)}
className="text-sm md:text-base h-9 md:h-10"
/>
<p className="text-[10px] md:text-xs text-muted-foreground">
{t("general_description_description")}
</p>
</div>
</CardContent>
</Card>
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="search-space-description"
className="text-sm md:text-base font-medium"
>
{t("general_description_label")}{" "}
<span className="text-muted-foreground font-normal">({tCommon("optional")})</span>
</Label>
<Input
id="search-space-description"
placeholder={t("general_description_placeholder")}
value={description}
onChange={(e) => setDescription(e.target.value)}
className="text-sm md:text-base h-9 md:h-10"
/>
<p className="text-[10px] md:text-xs text-muted-foreground">
{t("general_description_description")}
</p>
</div>
</CardContent>
</Card>
{/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4">
<Button
type="submit"
variant="outline"
disabled={!hasChanges || saving || !name.trim()}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
{saving ? <Spinner size="sm" /> : null}
{saving ? t("general_saving") : t("general_save")}
</Button>
</div>
</form>
</div>
);
{/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4">
<Button
type="submit"
variant="outline"
disabled={!hasChanges || saving || !name.trim()}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
{saving ? <Spinner size="sm" /> : null}
{saving ? t("general_saving") : t("general_save")}
</Button>
</div>
</form>
</div>
);
}

View file

@ -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 (
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<Skeleton className="h-16 md:h-20 w-full" />
<Skeleton className="h-24 md:h-32 w-full" />
</CardContent>
</Card>
</div>
);
}
if (loading) {
return (
<div className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<Skeleton className="h-5 md:h-6 w-36 md:w-48" />
<Skeleton className="h-3 md:h-4 w-full max-w-md mt-2" />
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<Skeleton className="h-16 md:h-20 w-full" />
<Skeleton className="h-24 md:h-32 w-full" />
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-4 md:space-y-6">
{/* Work in Progress Notice */}
<Alert
variant="default"
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700 py-3 md:py-4"
>
<AlertTriangle className="h-3 w-3 md:h-4 md:w-4 text-amber-600 dark:text-amber-500 shrink-0" />
<AlertDescription className="text-amber-800 dark:text-amber-300 text-xs md:text-sm">
<span className="font-semibold">Work in Progress:</span> 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.
</AlertDescription>
</Alert>
return (
<div className="space-y-4 md:space-y-6">
{/* Work in Progress Notice */}
<Alert
variant="default"
className="bg-amber-50 dark:bg-amber-950/30 border-amber-300 dark:border-amber-700 py-3 md:py-4"
>
<AlertTriangle className="h-3 w-3 md:h-4 md:w-4 text-amber-600 dark:text-amber-500 shrink-0" />
<AlertDescription className="text-amber-800 dark:text-amber-300 text-xs md:text-sm">
<span className="font-semibold">Work in Progress:</span> 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.
</AlertDescription>
</Alert>
<Alert className="bg-muted/50 py-3 md:py-4">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
System instructions apply to all AI interactions in this search space.
They guide how the AI responds, its tone, focus areas, and behavior
patterns.
</AlertDescription>
</Alert>
<Alert className="bg-muted/50 py-3 md:py-4">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
System instructions apply to all AI interactions in this search space. They guide how the
AI responds, its tone, focus areas, and behavior patterns.
</AlertDescription>
</Alert>
{/* System Instructions Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">
Custom System Instructions
</CardTitle>
<CardDescription className="text-xs md:text-sm">
Provide specific guidelines for how you want the AI to respond.
These instructions will be applied to all answers in this search
space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="custom-instructions-settings"
className="text-sm md:text-base font-medium"
>
Your Instructions
</Label>
<Textarea
id="custom-instructions-settings"
placeholder="E.g., Always provide practical examples, be concise, focus on technical details, use simple language, respond in a specific format..."
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
rows={10}
className="resize-none font-mono text-xs md:text-sm"
/>
<div className="flex items-center justify-between">
<p className="text-[10px] md:text-xs text-muted-foreground">
{customInstructions.length} characters
</p>
{customInstructions.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setCustomInstructions("")}
className="h-auto py-0.5 md:py-1 px-1.5 md:px-2 text-[10px] md:text-xs"
>
Clear
</Button>
)}
</div>
</div>
{/* System Instructions Card */}
<form onSubmit={onSubmit} className="space-y-4 md:space-y-6">
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg">Custom System Instructions</CardTitle>
<CardDescription className="text-xs md:text-sm">
Provide specific guidelines for how you want the AI to respond. These instructions
will be applied to all answers in this search space.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 md:space-y-4 px-3 md:px-6 pb-3 md:pb-6">
<div className="space-y-1.5 md:space-y-2">
<Label
htmlFor="custom-instructions-settings"
className="text-sm md:text-base font-medium"
>
Your Instructions
</Label>
<Textarea
id="custom-instructions-settings"
placeholder="E.g., Always provide practical examples, be concise, focus on technical details, use simple language, respond in a specific format..."
value={customInstructions}
onChange={(e) => setCustomInstructions(e.target.value)}
rows={10}
className="resize-none font-mono text-xs md:text-sm"
/>
<div className="flex items-center justify-between">
<p className="text-[10px] md:text-xs text-muted-foreground">
{customInstructions.length} characters
</p>
{customInstructions.length > 0 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setCustomInstructions("")}
className="h-auto py-0.5 md:py-1 px-1.5 md:px-2 text-[10px] md:text-xs"
>
Clear
</Button>
)}
</div>
</div>
{customInstructions.trim().length === 0 && (
<Alert className="py-2 md:py-3">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
No system instructions are currently set. The AI will use
default behavior.
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{customInstructions.trim().length === 0 && (
<Alert className="py-2 md:py-3">
<Info className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
No system instructions are currently set. The AI will use default behavior.
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4">
<Button
type="submit"
variant="outline"
disabled={!hasChanges || saving}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
{saving ? <Spinner size="sm" /> : null}
{saving ? "Saving" : "Save Instructions"}
</Button>
</div>
</form>
</div>
);
{/* Action Buttons */}
<div className="flex justify-end pt-3 md:pt-4">
<Button
type="submit"
variant="outline"
disabled={!hasChanges || saving}
className="gap-2 bg-white text-black hover:bg-neutral-100 dark:bg-white dark:text-black dark:hover:bg-neutral-200"
>
{saving ? <Spinner size="sm" /> : null}
{saving ? "Saving" : "Save Instructions"}
</Button>
</div>
</form>
</div>
);
}

View file

@ -215,7 +215,7 @@ export const createAnimation = (
`,
};
}
if (variant === "circle" && start == "center") {
if (variant === "circle" && start === "center") {
return {
name: `${variant}-${start}${blur ? "-blur" : ""}`,
css: `

View file

@ -67,13 +67,14 @@ const SANDBOX_FILE_RE = /^SANDBOX_FILE:\s*(.+)$/gm;
function extractSandboxFiles(text: string): SandboxFile[] {
const files: SandboxFile[] = [];
let match: RegExpExecArray | null;
while ((match = SANDBOX_FILE_RE.exec(text)) !== null) {
let match: RegExpExecArray | null = SANDBOX_FILE_RE.exec(text);
while (match !== null) {
const filePath = match[1].trim();
if (filePath) {
const name = filePath.includes("/") ? filePath.split("/").pop() || filePath : filePath;
files.push({ path: filePath, name });
}
match = SANDBOX_FILE_RE.exec(text);
}
SANDBOX_FILE_RE.lastIndex = 0;
return files;
@ -148,7 +149,7 @@ function parseExecuteResult(result: ExecuteResult): ParsedOutput {
function truncateCommand(command: string, maxLen = 80): string {
if (command.length <= maxLen) return command;
return command.slice(0, maxLen) + "…";
return `${command.slice(0, maxLen)}`;
}
// ============================================================================

View file

@ -18,7 +18,7 @@ import {
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Tooltip, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
const UNDRAGGABLE_KEYS = [KEYS.column, KEYS.tr, KEYS.td];
@ -94,23 +94,24 @@ function Draggable(props: PlateElementProps) {
};
// clear up virtual multiple preview when drag end
// biome-ignore lint/correctness/useExhaustiveDependencies: resetPreview is stable; intentionally only run on isDragging change
React.useEffect(() => {
if (!isDragging) {
resetPreview();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDragging]);
// biome-ignore lint/correctness/useExhaustiveDependencies: previewRef is a stable ref; only run on isAboutToDrag change
React.useEffect(() => {
if (isAboutToDrag) {
previewRef.current?.classList.remove("opacity-0");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAboutToDrag]);
const [dragButtonTop, setDragButtonTop] = React.useState(0);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: plate editor block wrapper requires mouse events
<div
className={cn(
"relative",
@ -158,6 +159,7 @@ function Draggable(props: PlateElementProps) {
contentEditable={false}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: plate editor context menu handler */}
<div
ref={nodeRef}
className="slate-blockWrapper flow-root"
@ -215,8 +217,10 @@ const DragHandle = React.memo(function DragHandle({
return (
<Tooltip>
<TooltipTrigger asChild>
{/* biome-ignore lint/a11y/useSemanticElements: drag handle requires div for plate editor integration */}
<div
className="flex size-full items-center justify-center"
tabIndex={0}
onClick={(e) => {
e.preventDefault();
editor.getApi(BlockSelectionPlugin).blockSelection.focus();
@ -291,6 +295,12 @@ const DragHandle = React.memo(function DragHandle({
onMouseUp={() => {
resetPreview();
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
editor.getApi(BlockSelectionPlugin).blockSelection.focus();
}
}}
data-plate-prevent-deselect
role="button"
>

View file

@ -43,10 +43,16 @@ export function EquationElement({ children, ...props }: PlateElementProps<TEquat
props.className
)}
>
{/* biome-ignore lint/a11y/useSemanticElements: contentEditable context requires div */}
<div
role="button"
tabIndex={0}
className="flex cursor-pointer items-center justify-center"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
onKeyDown={(e) => {
if (e.key === "Enter") setIsEditing(true);
}}
>
{element.texExpression ? (
<div ref={katexRef} className="text-center" />
@ -123,10 +129,16 @@ export function InlineEquationElement({ children, ...props }: PlateElementProps<
as="span"
className={cn("inline rounded-sm px-0.5", selected && "bg-brand/15", props.className)}
>
{/* biome-ignore lint/a11y/useSemanticElements: inline contentEditable context requires span */}
<span
role="button"
tabIndex={0}
className="cursor-pointer"
contentEditable={false}
onDoubleClick={() => setIsEditing(true)}
onKeyDown={(e) => {
if (e.key === "Enter") setIsEditing(true);
}}
>
{element.texExpression ? (
<span ref={katexRef} />

View file

@ -97,7 +97,7 @@ function HeroCarouselCard({
observer.observe(video);
return () => observer.disconnect();
}, [src]);
}, []);
const handleCanPlay = useCallback(() => {
setHasLoaded(true);
@ -114,7 +114,19 @@ function HeroCarouselCard({
<p className="text-sm text-neutral-500 dark:text-neutral-400">{description}</p>
</div>
</div>
<div className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950" onClick={open}>
{/* biome-ignore lint/a11y/useSemanticElements: div wraps video element, button would break layout */}
<div
role="button"
tabIndex={0}
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950"
onClick={open}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
open();
}
}}
>
<div className="relative">
<video
ref={videoRef}
@ -185,45 +197,45 @@ function HeroCarousel() {
</AnimatePresence>
</div>
<div className="relative z-5 mt-4 flex items-center justify-center gap-2">
<button
type="button"
onClick={() => !isGifExpanded && goToPrev()}
className="flex size-11 items-center justify-center rounded-full border border-neutral-200 bg-white text-neutral-700 shadow-sm transition-colors hover:bg-neutral-100 touch-manipulation dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
aria-label="Previous slide"
>
<ChevronLeft className="size-5" />
</button>
<div className="relative z-5 mt-4 flex items-center justify-center gap-2">
<button
type="button"
onClick={() => !isGifExpanded && goToPrev()}
className="flex size-11 items-center justify-center rounded-full border border-neutral-200 bg-white text-neutral-700 shadow-sm transition-colors hover:bg-neutral-100 touch-manipulation dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
aria-label="Previous slide"
>
<ChevronLeft className="size-5" />
</button>
<div className="flex items-center">
{carouselItems.map((_, i) => (
<button
key={`dot_${i}`}
type="button"
onClick={() => !isGifExpanded && goTo(i)}
className="flex h-11 min-w-[28px] items-center justify-center touch-manipulation"
aria-label={`Go to slide ${i + 1}`}
>
<span
className={`block h-2.5 rounded-full transition-all duration-300 ${
i === activeIndex
? "w-6 bg-neutral-900 dark:bg-white"
: "w-2.5 bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-600 dark:hover:bg-neutral-500"
}`}
/>
</button>
))}
<div className="flex items-center">
{carouselItems.map((_, i) => (
<button
key={`dot_${i}`}
type="button"
onClick={() => !isGifExpanded && goTo(i)}
className="flex h-11 min-w-[28px] items-center justify-center touch-manipulation"
aria-label={`Go to slide ${i + 1}`}
>
<span
className={`block h-2.5 rounded-full transition-all duration-300 ${
i === activeIndex
? "w-6 bg-neutral-900 dark:bg-white"
: "w-2.5 bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-600 dark:hover:bg-neutral-500"
}`}
/>
</button>
))}
</div>
<button
type="button"
onClick={() => !isGifExpanded && goToNext()}
className="flex size-11 items-center justify-center rounded-full border border-neutral-200 bg-white text-neutral-700 shadow-sm transition-colors hover:bg-neutral-100 touch-manipulation dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
aria-label="Next slide"
>
<ChevronRight className="size-5" />
</button>
</div>
<button
type="button"
onClick={() => !isGifExpanded && goToNext()}
className="flex size-11 items-center justify-center rounded-full border border-neutral-200 bg-white text-neutral-700 shadow-sm transition-colors hover:bg-neutral-100 touch-manipulation dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-700"
aria-label="Next slide"
>
<ChevronRight className="size-5" />
</button>
</div>
</div>
);
}

View file

@ -160,22 +160,21 @@ function LinkOpenButton() {
const editor = useEditorRef();
const selection = useEditorSelection();
const attributes = React.useMemo(
() => {
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
if (!entry) {
return {};
}
const [element] = entry;
return getLinkAttributes(editor, element);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[editor, selection]
);
// biome-ignore lint/correctness/useExhaustiveDependencies: selection triggers recalculation of link attributes
const attributes = React.useMemo(() => {
const entry = editor.api.node<TLinkElement>({
match: { type: editor.getType(KEYS.link) },
});
if (!entry) {
return {};
}
const [element] = entry;
return getLinkAttributes(editor, element);
}, [editor, selection]);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: <a> with spread attributes has dynamic href
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label needed for icon-only link
<a
{...attributes}
className={buttonVariants({
@ -185,6 +184,9 @@ function LinkOpenButton() {
onMouseOver={(e) => {
e.stopPropagation();
}}
onFocus={(e) => {
e.stopPropagation();
}}
aria-label="Open link in a new tab"
target="_blank"
rel="noopener noreferrer"

View file

@ -219,7 +219,8 @@ export function ToolbarSplitButtonSecondary({
...props
}: React.ComponentPropsWithoutRef<"span"> & VariantProps<typeof dropdownArrowVariants>) {
return (
<span
<button
type="button"
className={cn(
dropdownArrowVariants({
size,
@ -229,11 +230,10 @@ export function ToolbarSplitButtonSecondary({
className
)}
onClick={(e) => e.stopPropagation()}
role="button"
{...props}
{...(props as React.ComponentPropsWithoutRef<"button">)}
>
<ChevronDown className="size-3.5 text-muted-foreground" data-icon />
</span>
</button>
);
}

View file

@ -13,7 +13,7 @@ export interface Log {
status: LogStatus;
message: string;
source?: string;
log_metadata?: Record<string, any>;
log_metadata?: Record<string, unknown>;
created_at: string;
search_space_id: number;
}
@ -52,8 +52,9 @@ export interface LogSummary {
}
export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
// Memoize filters to prevent infinite re-renders
const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]);
const filtersKey = JSON.stringify(filters);
// biome-ignore lint/correctness/useExhaustiveDependencies: stable serialized key used intentionally
const memoizedFilters = useMemo(() => filters, [filtersKey]);
const buildQueryParams = useCallback(
(customFilters: LogFilters = {}) => {
@ -62,22 +63,22 @@ export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) {
const allFilters = { ...memoizedFilters, ...customFilters };
if (allFilters.search_space_id) {
params["search_space_id"] = allFilters.search_space_id.toString();
params.search_space_id = allFilters.search_space_id.toString();
}
if (allFilters.level) {
params["level"] = allFilters.level;
params.level = allFilters.level;
}
if (allFilters.status) {
params["status"] = allFilters.status;
params.status = allFilters.status;
}
if (allFilters.source) {
params["source"] = allFilters.source;
params.source = allFilters.source;
}
if (allFilters.start_date) {
params["start_date"] = allFilters.start_date;
params.start_date = allFilters.start_date;
}
if (allFilters.end_date) {
params["end_date"] = allFilters.end_date;
params.end_date = allFilters.end_date;
}
return params;

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react";
import { authenticatedFetch, getBearerToken, handleUnauthorized } from "@/lib/auth-utils";
import { authenticatedFetch } from "@/lib/auth-utils";
export interface SearchSourceConnector {
id: number;
@ -7,7 +7,7 @@ export interface SearchSourceConnector {
connector_type: string;
is_indexable: boolean;
last_indexed_at: string | null;
config: Record<string, any>;
config: Record<string, unknown>;
search_space_id: number;
user_id?: string;
created_at?: string;
@ -20,7 +20,7 @@ export interface ConnectorSourceItem {
id: number;
name: string;
type: string;
sources: any[];
sources: unknown[];
}
/**
@ -60,6 +60,44 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
},
]);
const updateConnectorSourceItems = useCallback((currentConnectors: SearchSourceConnector[]) => {
const defaultConnectors: ConnectorSourceItem[] = [
{
id: 1,
name: "Crawled URL",
type: "CRAWLED_URL",
sources: [],
},
{
id: 2,
name: "File",
type: "FILE",
sources: [],
},
{
id: 3,
name: "Extension",
type: "EXTENSION",
sources: [],
},
{
id: 4,
name: "Youtube Video",
type: "YOUTUBE_VIDEO",
sources: [],
},
];
const apiConnectors: ConnectorSourceItem[] = currentConnectors.map((connector, index) => ({
id: 1000 + index,
name: connector.name,
type: connector.connector_type,
sources: [],
}));
setConnectorSourceItems([...defaultConnectors, ...apiConnectors]);
}, []);
const fetchConnectors = useCallback(
async (spaceId?: number) => {
if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode
@ -100,7 +138,7 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
setIsLoading(false);
}
},
[isLoaded, lazy]
[isLoaded, lazy, updateConnectorSourceItems]
);
useEffect(() => {
@ -120,47 +158,6 @@ export const useSearchSourceConnectors = (lazy: boolean = false, searchSpaceId?:
[fetchConnectors, searchSpaceId]
);
// Update connector source items when connectors change
const updateConnectorSourceItems = (currentConnectors: SearchSourceConnector[]) => {
// Start with the default hardcoded connectors
const defaultConnectors: ConnectorSourceItem[] = [
{
id: 1,
name: "Crawled URL",
type: "CRAWLED_URL",
sources: [],
},
{
id: 2,
name: "File",
type: "FILE",
sources: [],
},
{
id: 3,
name: "Extension",
type: "EXTENSION",
sources: [],
},
{
id: 4,
name: "Youtube Video",
type: "YOUTUBE_VIDEO",
sources: [],
},
];
// Add the API connectors
const apiConnectors: ConnectorSourceItem[] = currentConnectors.map((connector, index) => ({
id: 1000 + index, // Use a high ID to avoid conflicts with hardcoded IDs
name: connector.name,
type: connector.connector_type,
sources: [],
}));
setConnectorSourceItems([...defaultConnectors, ...apiConnectors]);
};
/**
* Create a new search source connector
* @param connectorData - The connector data (excluding search_space_id)

View file

@ -10,7 +10,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Ensure that the incoming `locale` is valid
if (!locale || !routing.locales.includes(locale as any)) {
if (!locale || !routing.locales.includes(locale as (typeof routing.locales)[number])) {
locale = routing.defaultLocale;
}

View file

@ -16,7 +16,7 @@ export type RequestOptions = {
headers?: Record<string, string>;
contentType?: "application/json" | "application/x-www-form-urlencoded";
signal?: AbortSignal;
body?: any;
body?: unknown;
responseType?: ResponseType;
_isRetry?: boolean; // Internal flag to prevent infinite retry loops
// Add more options as needed

View file

@ -139,7 +139,7 @@ class DocumentsApiService {
for (const batch of batches) {
const formData = new FormData();
batch.forEach((file) => formData.append("files", file));
for (const file of batch) formData.append("files", file);
formData.append("search_space_id", String(search_space_id));
formData.append("should_summarize", String(should_summarize));

View file

@ -1,12 +1,12 @@
import {
type BulkDocumentMoveRequest,
bulkDocumentMoveRequest,
type DocumentMoveRequest,
documentMoveRequest,
type FolderCreateRequest,
type FolderMoveRequest,
type FolderReorderRequest,
type FolderUpdateRequest,
bulkDocumentMoveRequest,
documentMoveRequest,
folder,
folderBreadcrumbResponse,
folderCreateRequest,
@ -23,7 +23,9 @@ class FoldersApiService {
createFolder = async (request: FolderCreateRequest) => {
const parsed = folderCreateRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.post("/api/v1/folders", folder, { body: parsed.data });
};
@ -31,7 +33,7 @@ class FoldersApiService {
listFolders = async (searchSpaceId: number) => {
return baseApiService.get(
`/api/v1/folders?search_space_id=${searchSpaceId}`,
folderListResponse,
folderListResponse
);
};
@ -40,16 +42,15 @@ class FoldersApiService {
};
getFolderBreadcrumb = async (folderId: number) => {
return baseApiService.get(
`/api/v1/folders/${folderId}/breadcrumb`,
folderBreadcrumbResponse,
);
return baseApiService.get(`/api/v1/folders/${folderId}/breadcrumb`, folderBreadcrumbResponse);
};
updateFolder = async (folderId: number, request: FolderUpdateRequest) => {
const parsed = folderUpdateRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.put(`/api/v1/folders/${folderId}`, folder, {
body: parsed.data,
@ -59,7 +60,9 @@ class FoldersApiService {
moveFolder = async (folderId: number, request: FolderMoveRequest) => {
const parsed = folderMoveRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.put(`/api/v1/folders/${folderId}/move`, folder, {
body: parsed.data,
@ -69,7 +72,9 @@ class FoldersApiService {
reorderFolder = async (folderId: number, request: FolderReorderRequest) => {
const parsed = folderReorderRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.put(`/api/v1/folders/${folderId}/reorder`, folder, {
body: parsed.data,
@ -77,16 +82,15 @@ class FoldersApiService {
};
deleteFolder = async (folderId: number) => {
return baseApiService.delete(
`/api/v1/folders/${folderId}`,
folderDeleteResponse,
);
return baseApiService.delete(`/api/v1/folders/${folderId}`, folderDeleteResponse);
};
moveDocument = async (documentId: number, request: DocumentMoveRequest) => {
const parsed = documentMoveRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.put(`/api/v1/documents/${documentId}/move`, undefined, {
body: parsed.data,
@ -96,7 +100,9 @@ class FoldersApiService {
bulkMoveDocuments = async (request: BulkDocumentMoveRequest) => {
const parsed = bulkDocumentMoveRequest.safeParse(request);
if (!parsed.success) {
throw new ValidationError(`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
throw new ValidationError(
`Invalid request: ${parsed.error.issues.map((i) => i.message).join(", ")}`
);
}
return baseApiService.put("/api/v1/documents/bulk-move", undefined, {
body: parsed.data,

View file

@ -1,26 +1,20 @@
import {
type AcceptInviteRequest,
type AcceptInviteResponse,
acceptInviteRequest,
acceptInviteResponse,
type CreateInviteRequest,
type CreateInviteResponse,
createInviteRequest,
createInviteResponse,
type DeleteInviteRequest,
type DeleteInviteResponse,
deleteInviteRequest,
deleteInviteResponse,
type GetInviteInfoRequest,
type GetInviteInfoResponse,
type GetInvitesRequest,
type GetInvitesResponse,
getInviteInfoRequest,
getInviteInfoResponse,
getInvitesRequest,
getInvitesResponse,
type UpdateInviteRequest,
type UpdateInviteResponse,
updateInviteRequest,
updateInviteResponse,
} from "@/contracts/types/invites.types";

View file

@ -14,8 +14,6 @@ import {
getLogSummaryResponse,
getLogsRequest,
getLogsResponse,
type Log,
log,
type UpdateLogRequest,
updateLogRequest,
updateLogResponse,

View file

@ -1,22 +1,17 @@
import {
type DeleteMembershipRequest,
type DeleteMembershipResponse,
deleteMembershipRequest,
deleteMembershipResponse,
type GetMembersRequest,
type GetMembersResponse,
type GetMyAccessRequest,
type GetMyAccessResponse,
getMembersRequest,
getMembersResponse,
getMyAccessRequest,
getMyAccessResponse,
type LeaveSearchSpaceRequest,
type LeaveSearchSpaceResponse,
leaveSearchSpaceRequest,
leaveSearchSpaceResponse,
type UpdateMembershipRequest,
type UpdateMembershipResponse,
updateMembershipRequest,
updateMembershipResponse,
} from "@/contracts/types/members.types";

View file

@ -136,7 +136,7 @@ export function buildContentForPersistence(
* Async generator that reads an SSE stream and yields parsed JSON objects.
* Handles buffering, event splitting, and skips malformed JSON / [DONE] lines.
*/
export async function* readSSEStream(response: Response): AsyncGenerator<any> {
export async function* readSSEStream(response: Response): AsyncGenerator<unknown> {
if (!response.body) {
throw new Error("No response body");
}

View file

@ -1,7 +1,7 @@
import path from "node:path";
import { createMDX } from "fumadocs-mdx/next";
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";
import path from "path";
// Create the next-intl plugin
const withNextIntl = createNextIntlPlugin("./i18n/request.ts");
@ -37,7 +37,9 @@ const nextConfig: NextConfig = {
// Configure webpack (SVGR)
webpack: (config) => {
// SVGR: import *.svg as React components
const fileLoaderRule = config.module.rules.find((rule: any) => rule.test?.test?.(".svg"));
const fileLoaderRule = config.module.rules.find(
(rule: { test?: { test?: (s: string) => boolean } }) => rule.test?.test?.(".svg")
);
config.module.rules.push(
// Re-apply the existing file loader for *.svg?url imports
{