From b5e20e7515b34a0a28d213b52da8fa2667bd92b6 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Sun, 21 Dec 2025 16:32:55 -0800 Subject: [PATCH] feat: old chat to new-chat with persistance --- .../app/routes/new_chat_routes.py | 76 +++++- .../dashboard/[search_space_id]/layout.tsx | 2 +- .../new-chat/[[...chat_id]]/page.tsx | 45 ++-- .../assistant-ui/inline-citation.tsx | 1 - .../components/assistant-ui/markdown-text.tsx | 20 +- .../components/assistant-ui/thread-list.tsx | 23 +- surfsense_web/components/markdown-viewer.tsx | 22 +- .../new-chat/source-detail-panel.tsx | 216 +++++++++--------- .../components/sidebar/AppSidebarProvider.tsx | 131 +++++------ .../components/sidebar/all-chats-sidebar.tsx | 212 ++++++++++++----- surfsense_web/components/tool-ui/audio.tsx | 47 +--- .../components/tool-ui/generate-podcast.tsx | 19 +- surfsense_web/components/tool-ui/index.ts | 1 - surfsense_web/lib/chat/podcast-state.ts | 1 - surfsense_web/lib/chat/thread-persistence.ts | 50 ++-- surfsense_web/messages/en.json | 8 +- surfsense_web/tsconfig.json | 1 - 17 files changed, 490 insertions(+), 385 deletions(-) diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py index c3102db67..209c25a15 100644 --- a/surfsense_backend/app/routes/new_chat_routes.py +++ b/surfsense_backend/app/routes/new_chat_routes.py @@ -51,6 +51,7 @@ router = APIRouter() @router.get("/threads", response_model=ThreadListResponse) async def list_threads( search_space_id: int, + limit: int | None = None, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): @@ -58,6 +59,10 @@ async def list_threads( List all threads for the current user in a search space. Returns threads and archived_threads for ThreadListPrimitive. + Args: + search_space_id: The search space to list threads for + limit: Optional limit on number of threads to return (applies to active threads only) + Requires CHATS_READ permission. """ try: @@ -91,14 +96,18 @@ async def list_threads( id=thread.id, title=thread.title, archived=thread.archived, - createdAt=thread.created_at, - updatedAt=thread.updated_at, + created_at=thread.created_at, + updated_at=thread.updated_at, ) if thread.archived: archived_threads.append(item) else: threads.append(item) + # Apply limit to active threads if specified + if limit is not None and limit > 0: + threads = threads[:limit] + return ThreadListResponse(threads=threads, archived_threads=archived_threads) except HTTPException: @@ -114,6 +123,69 @@ async def list_threads( ) from None +@router.get("/threads/search", response_model=list[ThreadListItem]) +async def search_threads( + search_space_id: int, + title: str, + session: AsyncSession = Depends(get_async_session), + user: User = Depends(current_active_user), +): + """ + Search threads by title in a search space. + + Args: + search_space_id: The search space to search in + title: The search query (case-insensitive partial match) + + Requires CHATS_READ permission. + """ + try: + await check_permission( + session, + user, + search_space_id, + Permission.CHATS_READ.value, + "You don't have permission to read chats in this search space", + ) + + # Search threads by title (case-insensitive) + query = ( + select(NewChatThread) + .filter( + NewChatThread.search_space_id == search_space_id, + NewChatThread.user_id == user.id, + NewChatThread.title.ilike(f"%{title}%"), + ) + .order_by(NewChatThread.updated_at.desc()) + ) + + result = await session.execute(query) + threads = result.scalars().all() + + return [ + ThreadListItem( + id=thread.id, + title=thread.title, + archived=thread.archived, + created_at=thread.created_at, + updated_at=thread.updated_at, + ) + for thread in threads + ] + + except HTTPException: + raise + except OperationalError: + raise HTTPException( + status_code=503, detail="Database operation failed. Please try again later." + ) from None + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"An unexpected error occurred while searching threads: {e!s}", + ) from None + + @router.post("/threads", response_model=NewChatThreadRead) async def create_thread( thread: NewChatThreadCreate, diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index 814cf49f4..ce2c778c5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -29,7 +29,7 @@ export default function DashboardLayout({ const customNavMain = [ { title: "Chat", - url: `/dashboard/${search_space_id}/researcher`, + url: `/dashboard/${search_space_id}/new-chat`, icon: "SquareTerminal", items: [], }, 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 ab880813e..317209429 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 @@ -2,26 +2,26 @@ import { AssistantRuntimeProvider, - useExternalStoreRuntime, type ThreadMessageLike, + useExternalStoreRuntime, } from "@assistant-ui/react"; import { useParams, useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; import { Thread } from "@/components/assistant-ui/thread"; import { GeneratePodcastToolUI } from "@/components/tool-ui/generate-podcast"; -import { - createThread, - getThreadMessages, - appendMessage, - type MessageRecord, -} from "@/lib/chat/thread-persistence"; import { getBearerToken } from "@/lib/auth-utils"; -import { toast } from "sonner"; import { isPodcastGenerating, looksLikePodcastRequest, setActivePodcastTaskId, } from "@/lib/chat/podcast-state"; +import { + appendMessage, + createThread, + getThreadMessages, + type MessageRecord, +} from "@/lib/chat/thread-persistence"; /** * Convert backend message to assistant-ui ThreadMessageLike format @@ -223,8 +223,7 @@ export default function NewChatPage() { ]); try { - const backendUrl = - process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000"; + const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000"; // Build message history for context const messageHistory = messages @@ -232,11 +231,7 @@ export default function NewChatPage() { .map((m) => { let text = ""; for (const part of m.content) { - if ( - typeof part === "object" && - part.type === "text" && - "text" in part - ) { + if (typeof part === "object" && part.type === "text" && "text" in part) { text += part.text; } } @@ -296,9 +291,7 @@ export default function NewChatPage() { accumulatedText += parsed.delta; setMessages((prev) => prev.map((m) => - m.id === assistantMsgId - ? { ...m, content: buildContent() } - : m + m.id === assistantMsgId ? { ...m, content: buildContent() } : m ) ); break; @@ -311,9 +304,7 @@ export default function NewChatPage() { }); setMessages((prev) => prev.map((m) => - m.id === assistantMsgId - ? { ...m, content: buildContent() } - : m + m.id === assistantMsgId ? { ...m, content: buildContent() } : m ) ); break; @@ -329,9 +320,7 @@ export default function NewChatPage() { }); setMessages((prev) => prev.map((m) => - m.id === assistantMsgId - ? { ...m, content: buildContent() } - : m + m.id === assistantMsgId ? { ...m, content: buildContent() } : m ) ); break; @@ -351,9 +340,7 @@ export default function NewChatPage() { } setMessages((prev) => prev.map((m) => - m.id === assistantMsgId - ? { ...m, content: buildContent() } - : m + m.id === assistantMsgId ? { ...m, content: buildContent() } : m ) ); break; @@ -379,9 +366,7 @@ export default function NewChatPage() { appendMessage(threadId, { role: "assistant", content: finalContent, - }).catch((err) => - console.error("Failed to persist assistant message:", err) - ); + }).catch((err) => console.error("Failed to persist assistant message:", err)); } } catch (error) { if (error instanceof Error && error.name === "AbortError") { diff --git a/surfsense_web/components/assistant-ui/inline-citation.tsx b/surfsense_web/components/assistant-ui/inline-citation.tsx index cdecb13ff..065f37e8e 100644 --- a/surfsense_web/components/assistant-ui/inline-citation.tsx +++ b/surfsense_web/components/assistant-ui/inline-citation.tsx @@ -39,4 +39,3 @@ export const InlineCitation: FC = ({ chunkId, citationNumbe ); }; - diff --git a/surfsense_web/components/assistant-ui/markdown-text.tsx b/surfsense_web/components/assistant-ui/markdown-text.tsx index ff4bcbef5..41d6143b9 100644 --- a/surfsense_web/components/assistant-ui/markdown-text.tsx +++ b/surfsense_web/components/assistant-ui/markdown-text.tsx @@ -9,11 +9,10 @@ import { useIsMarkdownCodeBlock, } from "@assistant-ui/react-markdown"; import { CheckIcon, CopyIcon } from "lucide-react"; -import { type FC, type ReactNode, memo, useState } from "react"; +import { type FC, memo, type ReactNode, useState } from "react"; import remarkGfm from "remark-gfm"; - -import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { InlineCitation } from "@/components/assistant-ui/inline-citation"; +import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/utils"; // Citation pattern: [citation:CHUNK_ID] @@ -212,18 +211,12 @@ const defaultComponents = memoizeMarkdownComponents({ ), h6: ({ className, children, ...props }) => ( -
+
{processChildrenWithCitations(children)}
), p: ({ className, children, ...props }) => ( -

+

{processChildrenWithCitations(children)}

), @@ -236,10 +229,7 @@ const defaultComponents = memoizeMarkdownComponents({ ), blockquote: ({ className, children, ...props }) => ( -
+
{processChildrenWithCitations(children)}
), diff --git a/surfsense_web/components/assistant-ui/thread-list.tsx b/surfsense_web/components/assistant-ui/thread-list.tsx index de479e6b8..f65acd5c6 100644 --- a/surfsense_web/components/assistant-ui/thread-list.tsx +++ b/surfsense_web/components/assistant-ui/thread-list.tsx @@ -1,9 +1,15 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { + ArchiveIcon, + MessageSquareIcon, + MoreVerticalIcon, + PlusIcon, + RotateCcwIcon, + TrashIcon, +} from "lucide-react"; import { useRouter } from "next/navigation"; -import { ArchiveIcon, MessageSquareIcon, PlusIcon, TrashIcon, MoreVerticalIcon, RotateCcwIcon } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -13,10 +19,11 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { - type ThreadListItem, createThreadListManager, + type ThreadListItem, type ThreadListState, } from "@/lib/chat/thread-persistence"; +import { cn } from "@/lib/utils"; interface ThreadListProps { searchSpaceId: number; @@ -123,7 +130,13 @@ export function ThreadList({ searchSpaceId, currentThreadId, className }: Thread {/* Header with New Chat button */}

Conversations

-
diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index a0686e1ad..5318ba5d1 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; -import { Streamdown } from "streamdown"; import type { Components } from "react-markdown"; +import { Streamdown } from "streamdown"; import { cn } from "@/lib/utils"; interface MarkdownViewerProps { @@ -68,12 +68,8 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) { ), - th: ({ ...props }) => ( -
- ), - td: ({ ...props }) => ( - - ), + th: ({ ...props }) => , + td: ({ ...props }) => , code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ""); const isInline = !match; @@ -96,11 +92,13 @@ export function MarkdownViewer({ content, className }: MarkdownViewerProps) { }; return ( -
- +
+ {content}
diff --git a/surfsense_web/components/new-chat/source-detail-panel.tsx b/surfsense_web/components/new-chat/source-detail-panel.tsx index a076586a7..6e3e7cce0 100644 --- a/surfsense_web/components/new-chat/source-detail-panel.tsx +++ b/surfsense_web/components/new-chat/source-detail-panel.tsx @@ -1,26 +1,26 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { + BookOpen, ChevronDown, ChevronUp, ExternalLink, - Loader2, - X, FileText, Hash, - BookOpen, + Loader2, Sparkles, + X, } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import type React from "react"; -import { type ReactNode, forwardRef, useCallback, useEffect, useRef, useState } from "react"; +import { forwardRef, type ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { MarkdownViewer } from "@/components/markdown-viewer"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Badge } from "@/components/ui/badge"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { cn } from "@/lib/utils"; @@ -70,9 +70,7 @@ const ChunkCard = forwardRef( )} > {/* Cited indicator glow effect */} - {isCited && ( -
- )} + {isCited &&
} {/* Header */}
@@ -87,9 +85,7 @@ const ChunkCard = forwardRef( > {index + 1}
- - of {totalChunks} chunks - + of {totalChunks} chunks
{isCited && ( @@ -152,86 +148,97 @@ export function SourceDetailPanel({ const citedChunkIndex = documentData?.chunks?.findIndex((chunk) => chunk.id === chunkId) ?? -1; // Simple scroll function that scrolls to a chunk by index - const scrollToChunkByIndex = useCallback((chunkIndex: number, smooth = true) => { - const scrollContainer = scrollAreaRef.current; - if (!scrollContainer) return; + const scrollToChunkByIndex = useCallback( + (chunkIndex: number, smooth = true) => { + const scrollContainer = scrollAreaRef.current; + if (!scrollContainer) return; - const viewport = scrollContainer.querySelector( - '[data-radix-scroll-area-viewport]' - ) as HTMLElement | null; - if (!viewport) return; + const viewport = scrollContainer.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement | null; + if (!viewport) return; - const chunkElement = scrollContainer.querySelector( - `[data-chunk-index="${chunkIndex}"]` - ) as HTMLElement | null; - if (!chunkElement) return; + const chunkElement = scrollContainer.querySelector( + `[data-chunk-index="${chunkIndex}"]` + ) as HTMLElement | null; + if (!chunkElement) return; - // Get positions using getBoundingClientRect for accuracy - const viewportRect = viewport.getBoundingClientRect(); - const chunkRect = chunkElement.getBoundingClientRect(); + // Get positions using getBoundingClientRect for accuracy + const viewportRect = viewport.getBoundingClientRect(); + const chunkRect = chunkElement.getBoundingClientRect(); - // Calculate where to scroll to center the chunk - const currentScrollTop = viewport.scrollTop; - const chunkTopRelativeToViewport = chunkRect.top - viewportRect.top + currentScrollTop; - const scrollTarget = chunkTopRelativeToViewport - (viewportRect.height / 2) + (chunkRect.height / 2); + // Calculate where to scroll to center the chunk + const currentScrollTop = viewport.scrollTop; + const chunkTopRelativeToViewport = chunkRect.top - viewportRect.top + currentScrollTop; + const scrollTarget = + chunkTopRelativeToViewport - viewportRect.height / 2 + chunkRect.height / 2; - viewport.scrollTo({ - top: Math.max(0, scrollTarget), - behavior: smooth && !shouldReduceMotion ? "smooth" : "auto", - }); - - setActiveChunkIndex(chunkIndex); - }, [shouldReduceMotion]); - - // Callback ref for the cited chunk - scrolls when the element mounts - const citedChunkRefCallback = useCallback((node: HTMLDivElement | null) => { - if (node && !hasScrolledRef.current && open) { - hasScrolledRef.current = true; // Mark immediately to prevent duplicate scrolls - - // Store the node reference for the delayed scroll - const scrollToCitedChunk = () => { - const scrollContainer = scrollAreaRef.current; - if (!scrollContainer || !node.isConnected) return false; - - const viewport = scrollContainer.querySelector( - '[data-radix-scroll-area-viewport]' - ) as HTMLElement | null; - if (!viewport) return false; - - // Get positions - const viewportRect = viewport.getBoundingClientRect(); - const chunkRect = node.getBoundingClientRect(); - - // Calculate scroll position to center the chunk - const currentScrollTop = viewport.scrollTop; - const chunkTopRelativeToViewport = chunkRect.top - viewportRect.top + currentScrollTop; - const scrollTarget = chunkTopRelativeToViewport - (viewportRect.height / 2) + (chunkRect.height / 2); - - viewport.scrollTo({ - top: Math.max(0, scrollTarget), - behavior: "auto", // Instant scroll for initial positioning - }); - - return true; - }; - - // Scroll multiple times with delays to handle progressive content rendering - // Each subsequent scroll will correct for any layout shifts - const scrollAttempts = [50, 150, 300, 600, 1000]; - - scrollAttempts.forEach((delay) => { - setTimeout(() => { - scrollToCitedChunk(); - }, delay); + viewport.scrollTo({ + top: Math.max(0, scrollTarget), + behavior: smooth && !shouldReduceMotion ? "smooth" : "auto", }); - // After final attempt, mark state as scrolled - setTimeout(() => { - setHasScrolledToCited(true); - setActiveChunkIndex(citedChunkIndex); - }, scrollAttempts[scrollAttempts.length - 1] + 50); - } - }, [open, citedChunkIndex]); + setActiveChunkIndex(chunkIndex); + }, + [shouldReduceMotion] + ); + + // Callback ref for the cited chunk - scrolls when the element mounts + const citedChunkRefCallback = useCallback( + (node: HTMLDivElement | null) => { + if (node && !hasScrolledRef.current && open) { + hasScrolledRef.current = true; // Mark immediately to prevent duplicate scrolls + + // Store the node reference for the delayed scroll + const scrollToCitedChunk = () => { + const scrollContainer = scrollAreaRef.current; + if (!scrollContainer || !node.isConnected) return false; + + const viewport = scrollContainer.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLElement | null; + if (!viewport) return false; + + // Get positions + const viewportRect = viewport.getBoundingClientRect(); + const chunkRect = node.getBoundingClientRect(); + + // Calculate scroll position to center the chunk + const currentScrollTop = viewport.scrollTop; + const chunkTopRelativeToViewport = chunkRect.top - viewportRect.top + currentScrollTop; + const scrollTarget = + chunkTopRelativeToViewport - viewportRect.height / 2 + chunkRect.height / 2; + + viewport.scrollTo({ + top: Math.max(0, scrollTarget), + behavior: "auto", // Instant scroll for initial positioning + }); + + return true; + }; + + // Scroll multiple times with delays to handle progressive content rendering + // Each subsequent scroll will correct for any layout shifts + const scrollAttempts = [50, 150, 300, 600, 1000]; + + scrollAttempts.forEach((delay) => { + setTimeout(() => { + scrollToCitedChunk(); + }, delay); + }); + + // After final attempt, mark state as scrolled + setTimeout( + () => { + setHasScrolledToCited(true); + setActiveChunkIndex(citedChunkIndex); + }, + scrollAttempts[scrollAttempts.length - 1] + 50 + ); + } + }, + [open, citedChunkIndex] + ); // Reset scroll state when panel closes useEffect(() => { @@ -271,9 +278,12 @@ export function SourceDetailPanel({ window.open(clickUrl, "_blank", "noopener,noreferrer"); }; - const scrollToChunk = useCallback((index: number) => { - scrollToChunkByIndex(index, true); - }, [scrollToChunkByIndex]); + const scrollToChunk = useCallback( + (index: number) => { + scrollToChunkByIndex(index, true); + }, + [scrollToChunkByIndex] + ); const panelContent = ( @@ -320,7 +330,8 @@ export function SourceDetailPanel({ : sourceType && formatDocumentType(sourceType)} {documentData?.chunks && ( - • {documentData.chunks.length} chunk{documentData.chunks.length !== 1 ? "s" : ""} + • {documentData.chunks.length} chunk + {documentData.chunks.length !== 1 ? "s" : ""} )}

@@ -378,9 +389,12 @@ export function SourceDetailPanel({
-

Failed to load document

+

+ Failed to load document +

- {documentByChunkFetchingError.message || "An unexpected error occurred. Please try again."} + {documentByChunkFetchingError.message || + "An unexpected error occurred. Please try again."}

+ {/* Tab toggle for active/archived (only show when not searching) */} + {!isSearchMode && ( +
+ + +
+ )} +
{isLoading ? ( @@ -160,19 +231,21 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS
{t("error_loading_chats") || "Error loading chats"}
- ) : chats.length > 0 ? ( + ) : threads.length > 0 ? (
- {chats.map((chat) => { - const isDeleting = deletingChatId === chat.id; + {threads.map((thread) => { + const isDeleting = deletingThreadId === thread.id; + const isArchiving = archivingThreadId === thread.id; + const isBusy = isDeleting || isArchiving; return (
{/* Main clickable area for navigation */} @@ -180,23 +253,23 @@ export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsS

- {t("created") || "Created"}:{" "} - {format(new Date(chat.created_at), "MMM d, yyyy 'at' h:mm a")} + {t("updated") || "Updated"}:{" "} + {format(new Date(thread.updatedAt), "MMM d, yyyy 'at' h:mm a")}

- {/* Actions dropdown - separate from main click area */} + {/* Actions dropdown */}
diff --git a/surfsense_web/components/tool-ui/audio.tsx b/surfsense_web/components/tool-ui/audio.tsx index 8adf8c498..19098d5b8 100644 --- a/surfsense_web/components/tool-ui/audio.tsx +++ b/surfsense_web/components/tool-ui/audio.tsx @@ -25,15 +25,7 @@ function formatTime(seconds: number): string { return `${mins}:${secs.toString().padStart(2, "0")}`; } -export function Audio({ - id, - src, - title, - description, - artwork, - durationMs, - className, -}: AudioProps) { +export function Audio({ id, src, title, description, artwork, durationMs, className }: AudioProps) { const audioRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); @@ -158,7 +150,7 @@ export function Audio({
@@ -177,7 +169,7 @@ export function Audio({ id={id} className={cn( "group relative overflow-hidden rounded-xl border bg-gradient-to-br from-background to-muted/30 p-4 shadow-sm transition-all hover:shadow-md", - className, + className )} > {/* Hidden audio element */} @@ -190,13 +182,7 @@ export function Audio({
{artwork ? ( - {title} + {title} ) : (
@@ -224,9 +210,7 @@ export function Audio({

{title}

{description && ( -

- {description} -

+

{description}

)}
@@ -271,17 +255,8 @@ export function Audio({ {/* Volume control */}
- {/* Download button */} - @@ -307,4 +277,3 @@ export function Audio({
); } - diff --git a/surfsense_web/components/tool-ui/generate-podcast.tsx b/surfsense_web/components/tool-ui/generate-podcast.tsx index 459307cb6..9f7248082 100644 --- a/surfsense_web/components/tool-ui/generate-podcast.tsx +++ b/surfsense_web/components/tool-ui/generate-podcast.tsx @@ -4,13 +4,10 @@ import { makeAssistantToolUI } from "@assistant-ui/react"; import { AlertCircleIcon, Loader2Icon, MicIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { Audio } from "@/components/tool-ui/audio"; +import type { PodcastTranscriptEntry } from "@/contracts/types/podcast.types"; import { baseApiService } from "@/lib/apis/base-api.service"; import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; -import { - clearActivePodcastTaskId, - setActivePodcastTaskId, -} from "@/lib/chat/podcast-state"; -import type { PodcastTranscriptEntry } from "@/contracts/types/podcast.types"; +import { clearActivePodcastTaskId, setActivePodcastTaskId } from "@/lib/chat/podcast-state"; /** * Type definitions for the generate_podcast tool @@ -223,9 +220,7 @@ function PodcastPlayer({
{transcript.map((entry, idx) => (
- - Speaker {entry.speaker_id + 1}: - {" "} + Speaker {entry.speaker_id + 1}:{" "} {entry.dialog}
))} @@ -239,13 +234,7 @@ function PodcastPlayer({ /** * Polling component that checks task status and shows player when complete */ -function PodcastTaskPoller({ - taskId, - title, -}: { - taskId: string; - title: string; -}) { +function PodcastTaskPoller({ taskId, title }: { taskId: string; title: string }) { const [taskStatus, setTaskStatus] = useState({ status: "processing" }); const pollingRef = useRef(null); diff --git a/surfsense_web/components/tool-ui/index.ts b/surfsense_web/components/tool-ui/index.ts index 4007a39fe..6125f625f 100644 --- a/surfsense_web/components/tool-ui/index.ts +++ b/surfsense_web/components/tool-ui/index.ts @@ -8,4 +8,3 @@ export { Audio } from "./audio"; export { GeneratePodcastToolUI } from "./generate-podcast"; - diff --git a/surfsense_web/lib/chat/podcast-state.ts b/surfsense_web/lib/chat/podcast-state.ts index 782a31bf6..061a89b63 100644 --- a/surfsense_web/lib/chat/podcast-state.ts +++ b/surfsense_web/lib/chat/podcast-state.ts @@ -71,4 +71,3 @@ export function looksLikePodcastRequest(message: string): boolean { return podcastPatterns.some((pattern) => pattern.test(message)); } - diff --git a/surfsense_web/lib/chat/thread-persistence.ts b/surfsense_web/lib/chat/thread-persistence.ts index f25c47c87..b84c5907b 100644 --- a/surfsense_web/lib/chat/thread-persistence.ts +++ b/surfsense_web/lib/chat/thread-persistence.ts @@ -51,11 +51,26 @@ export interface ThreadHistoryLoadResponse { * Fetch list of threads for a search space */ export async function fetchThreads( - searchSpaceId: number + searchSpaceId: number, + limit?: number ): Promise { - return baseApiService.get( - `/api/v1/threads?search_space_id=${searchSpaceId}` - ); + const params = new URLSearchParams({ search_space_id: String(searchSpaceId) }); + if (limit) params.append("limit", String(limit)); + return baseApiService.get(`/api/v1/threads?${params}`); +} + +/** + * Search threads by title + */ +export async function searchThreads( + searchSpaceId: number, + title: string +): Promise { + const params = new URLSearchParams({ + search_space_id: String(searchSpaceId), + title, + }); + return baseApiService.get(`/api/v1/threads/search?${params}`); } /** @@ -77,12 +92,8 @@ export async function createThread( /** * Get thread messages */ -export async function getThreadMessages( - threadId: number -): Promise { - return baseApiService.get( - `/api/v1/threads/${threadId}` - ); +export async function getThreadMessages(threadId: number): Promise { + return baseApiService.get(`/api/v1/threads/${threadId}`); } /** @@ -92,11 +103,9 @@ export async function appendMessage( threadId: number, message: { role: "user" | "assistant" | "system"; content: unknown } ): Promise { - return baseApiService.post( - `/api/v1/threads/${threadId}/messages`, - undefined, - { body: message } - ); + return baseApiService.post(`/api/v1/threads/${threadId}/messages`, undefined, { + body: message, + }); } /** @@ -106,11 +115,9 @@ export async function updateThread( threadId: number, updates: { title?: string; archived?: boolean } ): Promise { - return baseApiService.put( - `/api/v1/threads/${threadId}`, - undefined, - { body: updates } - ); + return baseApiService.put(`/api/v1/threads/${threadId}`, undefined, { + body: updates, + }); } /** @@ -159,8 +166,7 @@ export function createThreadListManager(config: ThreadListAdapterConfig) { threads: [], archivedThreads: [], isLoading: false, - error: - error instanceof Error ? error.message : "Failed to load threads", + error: error instanceof Error ? error.message : "Failed to load threads", }; } }, diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 43ec2b0aa..63ade6802 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -669,7 +669,13 @@ "more_options": "More options", "clear_search": "Clear search", "view_all_notes": "View all notes", - "add_note": "Add note" + "add_note": "Add note", + "archive": "Archive", + "unarchive": "Restore", + "chat_archived": "Chat archived", + "chat_unarchived": "Chat restored", + "no_archived_chats": "No archived chats", + "error_archiving_chat": "Failed to archive chat" }, "errors": { "something_went_wrong": "Something went wrong", diff --git a/surfsense_web/tsconfig.json b/surfsense_web/tsconfig.json index faa819807..06c759b45 100644 --- a/surfsense_web/tsconfig.json +++ b/surfsense_web/tsconfig.json @@ -33,4 +33,3 @@ ], "exclude": ["node_modules"] } -