From acf0396aa51a0c989795cebde336853285c175a4 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 23 Dec 2025 02:55:37 -0800 Subject: [PATCH 1/7] feat: fixed migrations - Migrated data from the old 'chats' table to 'new_chat_threads' and 'new_chat_messages'. - Dropped the 'chats' table and removed the 'chattype' enum as part of the migration process. - Updated the migration script to truncate thread titles to 500 characters to comply with database constraints. - Adjusted the downgrade function to reflect changes in the migration process. --- .../49_migrate_old_chats_to_new_chat.py | 51 ++++--------------- .../versions/53_cleanup_old_llm_configs.py | 42 ++------------- 2 files changed, 16 insertions(+), 77 deletions(-) diff --git a/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py b/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py index bacb33f05..50924ab65 100644 --- a/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py +++ b/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py @@ -6,9 +6,8 @@ Create Date: 2025-12-21 This migration: 1. Migrates data from old 'chats' table to 'new_chat_threads' and 'new_chat_messages' -2. Drops the 'podcasts' table (podcast data is not migrated as per user request) -3. Drops the 'chats' table -4. Removes the 'chattype' enum +2. Drops the 'chats' table +3. Removes the 'chattype' enum """ import json @@ -92,7 +91,11 @@ def upgrade() -> None: print(f"[Migration 49] Skipping empty chat {chat_id}") continue - # Create new thread + # Create new thread - truncate title to 500 chars (VARCHAR(500) limit) + thread_title = title or "Migrated Chat" + if len(thread_title) > 500: + thread_title = thread_title[:497] + "..." + result = connection.execute( sa.text(""" INSERT INTO new_chat_threads @@ -101,7 +104,7 @@ def upgrade() -> None: RETURNING id """), { - "title": title or "Migrated Chat", + "title": thread_title, "search_space_id": search_space_id, "created_at": created_at, }, @@ -162,11 +165,7 @@ def upgrade() -> None: print(f"[Migration 49] Successfully migrated {migrated_count} chats") - # Drop podcasts table (FK references chats, so drop first) - print("[Migration 49] Dropping podcasts table...") - op.drop_table("podcasts") - - # Drop chats table + # Drop chats table (podcasts table was already updated to remove chat_id FK) print("[Migration 49] Dropping chats table...") op.drop_table("chats") @@ -178,7 +177,7 @@ def upgrade() -> None: def downgrade() -> None: - """Recreate old tables (data cannot be restored).""" + """Recreate old chats table (data cannot be restored).""" # Recreate chattype enum op.execute( sa.text(""" @@ -209,32 +208,4 @@ def downgrade() -> None: ), ) - # Recreate podcasts table - op.create_table( - "podcasts", - sa.Column("id", sa.Integer(), primary_key=True, index=True), - sa.Column("title", sa.String(), nullable=False, index=True), - sa.Column("podcast_transcript", sa.JSON(), nullable=False, server_default="{}"), - sa.Column("file_location", sa.String(500), nullable=False, server_default=""), - sa.Column( - "chat_id", - sa.Integer(), - sa.ForeignKey("chats.id", ondelete="CASCADE"), - nullable=True, - ), - sa.Column("chat_state_version", sa.BigInteger(), nullable=True), - sa.Column( - "search_space_id", - sa.Integer(), - sa.ForeignKey("searchspaces.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "created_at", - sa.TIMESTAMP(timezone=True), - nullable=False, - server_default=sa.func.now(), - ), - ) - - print("[Migration 49 Downgrade] Tables recreated (data not restored)") + print("[Migration 49 Downgrade] Chats table recreated (data not restored)") diff --git a/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py b/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py index 16f5779be..0845a5093 100644 --- a/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py +++ b/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py @@ -6,8 +6,8 @@ Create Date: 2024-12-22 This migration: 1. Migrates data from old llm_configs table to new_llm_configs (preserving user configs) -2. Drops the old llm_configs table (no longer used) -3. Removes the is_default column from new_llm_configs (roles now determine which config to use) +2. Updates searchspaces to point to migrated configs +3. Drops the old llm_configs table (no longer used) """ from alembic import op @@ -47,7 +47,6 @@ def upgrade(): system_instructions, use_default_system_instructions, citations_enabled, - is_default, search_space_id, created_at ) @@ -59,11 +58,10 @@ def upgrade(): lc.model_name, lc.api_key, lc.api_base, - COALESCE(lc.litellm_params, '{}'::jsonb), + COALESCE(lc.litellm_params::json, '{}'::json), '' as system_instructions, -- Use defaults TRUE as use_default_system_instructions, TRUE as citations_enabled, - FALSE as is_default, lc.search_space_id, COALESCE(lc.created_at, NOW()) FROM llm_configs lc @@ -130,23 +128,7 @@ def upgrade(): """ ) - # STEP 3: Drop the is_default column from new_llm_configs - # (role assignments now determine which config to use) - op.execute( - """ - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'new_llm_configs' AND column_name = 'is_default' - ) THEN - ALTER TABLE new_llm_configs DROP COLUMN is_default; - END IF; - END$$; - """ - ) - - # STEP 4: Drop the old llm_configs table (data has been migrated) + # STEP 3: Drop the old llm_configs table (data has been migrated) op.execute("DROP TABLE IF EXISTS llm_configs CASCADE") @@ -213,7 +195,7 @@ def downgrade(): nlc.api_key, nlc.api_base, 'English' as language, -- Default language - COALESCE(nlc.litellm_params, '{}'::jsonb), + COALESCE(nlc.litellm_params::jsonb, '{}'::jsonb), nlc.search_space_id, nlc.created_at FROM new_llm_configs nlc @@ -228,17 +210,3 @@ def downgrade(): """ ) - # Add back the is_default column to new_llm_configs - op.execute( - """ - DO $$ - BEGIN - IF NOT EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_name = 'new_llm_configs' AND column_name = 'is_default' - ) THEN - ALTER TABLE new_llm_configs ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT FALSE; - END IF; - END$$; - """ - ) From 0b86756082904118fb6ec565219a7d7903fd2290 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 23 Dec 2025 02:56:13 -0800 Subject: [PATCH 2/7] chore: ruff format --- .../alembic/versions/49_migrate_old_chats_to_new_chat.py | 2 +- .../alembic/versions/53_cleanup_old_llm_configs.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py b/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py index 50924ab65..61a3ddb48 100644 --- a/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py +++ b/surfsense_backend/alembic/versions/49_migrate_old_chats_to_new_chat.py @@ -95,7 +95,7 @@ def upgrade() -> None: thread_title = title or "Migrated Chat" if len(thread_title) > 500: thread_title = thread_title[:497] + "..." - + result = connection.execute( sa.text(""" INSERT INTO new_chat_threads diff --git a/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py b/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py index 0845a5093..22f48c3ab 100644 --- a/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py +++ b/surfsense_backend/alembic/versions/53_cleanup_old_llm_configs.py @@ -209,4 +209,3 @@ def downgrade(): END$$; """ ) - From ed63e5a1d6b73fc12a0bdfd19797a62975b275f0 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:09:24 +0530 Subject: [PATCH 3/7] feat: add auto-scroll functionality for new user queries - Introduced NewQueryScrollHandler component to automatically scroll to the latest user message when a new query is submitted, enhancing user experience. - Updated ChatHeader component to remove unnecessary border for a cleaner design. - Adjusted ModelSelector styles for improved visual consistency and accessibility. --- .../components/assistant-ui/thread.tsx | 51 +++++++++++++++++++ .../components/new-chat/chat-header.tsx | 2 +- .../components/new-chat/model-selector.tsx | 16 +++--- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 33d6a0cad..8a7eca58e 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -262,6 +262,55 @@ const ThinkingStepsScrollHandler: FC = () => { return null; // This component doesn't render anything }; +/** + * Component that handles auto-scroll when a new user query is submitted. + * Scrolls to position the new user message at the top of the viewport, + * similar to Cursor's behavior where the current query stays at the top. + */ +const NewQueryScrollHandler: FC = () => { + const messages = useAssistantState(({ thread }) => thread.messages); + const prevMessageCountRef = useRef(0); + const prevLastUserMsgIdRef = useRef(""); + + useEffect(() => { + const currentCount = messages.length; + + // Find the last user message + const lastUserMessage = [...messages].reverse().find((m) => m.role === "user"); + const lastUserMsgId = lastUserMessage?.id || ""; + + // Check if a new user message was added (not on initial load) + if ( + prevMessageCountRef.current > 0 && // Not initial load + currentCount > prevMessageCountRef.current && + lastUserMsgId !== prevLastUserMsgIdRef.current && + lastUserMsgId + ) { + // New user message added - scroll to make it visible at the top + // Use multiple attempts to ensure the DOM has updated + const scrollToNewQuery = () => { + // Find the last user message element + const userMessages = document.querySelectorAll('[data-role="user"]'); + const lastUserMsgElement = userMessages[userMessages.length - 1]; + if (lastUserMsgElement) { + // Scroll so the user message is at the top of the viewport + lastUserMsgElement.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }; + + // Delayed attempts to handle async DOM updates + requestAnimationFrame(scrollToNewQuery); + setTimeout(scrollToNewQuery, 50); + setTimeout(scrollToNewQuery, 150); + } + + prevMessageCountRef.current = currentCount; + prevLastUserMsgIdRef.current = lastUserMsgId; + }, [messages]); + + return null; // This component doesn't render anything +}; + export const Thread: FC = ({ messageThinkingSteps = new Map() }) => { return ( @@ -275,6 +324,8 @@ export const Thread: FC = ({ messageThinkingSteps = new Map() }) => turnAnchor="top" className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4" > + {/* Auto-scroll handler for new queries - positions new user messages at the top */} + {/* Auto-scroll handler for thinking steps - must be inside Viewport */} diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index ef1533e23..1e0259447 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -48,7 +48,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) { return ( <> {/* Header Bar */} -
+
diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index 89390f957..2af476e3b 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -175,9 +175,10 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp role="combobox" aria-expanded={open} className={cn( - "h-9 gap-2 px-3 rounded-xl border border-border/50 bg-background/50 backdrop-blur-sm", - "hover:bg-muted/80 hover:border-border transition-all duration-200", + "h-9 gap-2 px-3 rounded-xl border border-border/30 bg-background/50 backdrop-blur-sm", + "hover:bg-muted/80 hover:border-border/30 transition-all duration-200", "text-sm font-medium text-foreground", + "focus-visible:ring-0 focus-visible:ring-offset-0", className )} > @@ -206,11 +207,11 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp - + {/* Switching overlay */} {isSwitching && (
@@ -221,8 +222,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp
)} -
- +
0 && filteredUserConfigs.length > 0 && ( - + )} {/* User Configs Section */} @@ -362,7 +362,7 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp )} {/* Add New Config Button */} -
+
-
{children}
+
{children}
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 00514facb..e18629b92 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 @@ -626,11 +626,11 @@ export default function NewChatPage() { -
- -
- -
+
+ } + />
); diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 8a7eca58e..6e69296e5 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -8,7 +8,6 @@ import { ThreadPrimitive, useAssistantState, useMessage, - useThreadViewport, } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { @@ -69,6 +68,8 @@ import { cn } from "@/lib/utils"; */ interface ThreadProps { messageThinkingSteps?: Map; + /** Optional header component to render at the top of the viewport (sticky) */ + header?: React.ReactNode; } // Context to pass thinking steps to AssistantMessage @@ -212,122 +213,25 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea ); }; -/** - * Component that handles auto-scroll when thinking steps update. - * Uses useThreadViewport to scroll to bottom when thinking steps change, - * ensuring the user always sees the latest content during streaming. - */ -const ThinkingStepsScrollHandler: FC = () => { - const thinkingStepsMap = useContext(ThinkingStepsContext); - const viewport = useThreadViewport(); - const isRunning = useAssistantState(({ thread }) => thread.isRunning); - // Track the serialized state to detect any changes - const prevStateRef = useRef(""); - - useEffect(() => { - // Only act during streaming - if (!isRunning) { - prevStateRef.current = ""; - return; - } - - // Serialize the thinking steps state to detect any changes - // This catches new steps, status changes, and item additions - let stateString = ""; - thinkingStepsMap.forEach((steps, msgId) => { - steps.forEach((step) => { - stateString += `${msgId}:${step.id}:${step.status}:${step.items?.length || 0};`; - }); - }); - - // If state changed at all during streaming, scroll - if (stateString !== prevStateRef.current && stateString !== "") { - prevStateRef.current = stateString; - - // Multiple attempts to ensure scroll happens after DOM updates - const scrollAttempt = () => { - try { - viewport.scrollToBottom(); - } catch (e) { - // Ignore errors - viewport might not be ready - } - }; - - // Delayed attempts to handle async DOM updates - requestAnimationFrame(scrollAttempt); - setTimeout(scrollAttempt, 100); - } - }, [thinkingStepsMap, viewport, isRunning]); - - return null; // This component doesn't render anything -}; - -/** - * Component that handles auto-scroll when a new user query is submitted. - * Scrolls to position the new user message at the top of the viewport, - * similar to Cursor's behavior where the current query stays at the top. - */ -const NewQueryScrollHandler: FC = () => { - const messages = useAssistantState(({ thread }) => thread.messages); - const prevMessageCountRef = useRef(0); - const prevLastUserMsgIdRef = useRef(""); - - useEffect(() => { - const currentCount = messages.length; - - // Find the last user message - const lastUserMessage = [...messages].reverse().find((m) => m.role === "user"); - const lastUserMsgId = lastUserMessage?.id || ""; - - // Check if a new user message was added (not on initial load) - if ( - prevMessageCountRef.current > 0 && // Not initial load - currentCount > prevMessageCountRef.current && - lastUserMsgId !== prevLastUserMsgIdRef.current && - lastUserMsgId - ) { - // New user message added - scroll to make it visible at the top - // Use multiple attempts to ensure the DOM has updated - const scrollToNewQuery = () => { - // Find the last user message element - const userMessages = document.querySelectorAll('[data-role="user"]'); - const lastUserMsgElement = userMessages[userMessages.length - 1]; - if (lastUserMsgElement) { - // Scroll so the user message is at the top of the viewport - lastUserMsgElement.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }; - - // Delayed attempts to handle async DOM updates - requestAnimationFrame(scrollToNewQuery); - setTimeout(scrollToNewQuery, 50); - setTimeout(scrollToNewQuery, 150); - } - - prevMessageCountRef.current = currentCount; - prevLastUserMsgIdRef.current = lastUserMsgId; - }, [messages]); - - return null; // This component doesn't render anything -}; - -export const Thread: FC = ({ messageThinkingSteps = new Map() }) => { +export const Thread: FC = ({ messageThinkingSteps = new Map(), header }) => { return ( - {/* Auto-scroll handler for new queries - positions new user messages at the top */} - - {/* Auto-scroll handler for thinking steps - must be inside Viewport */} - + {/* Optional sticky header for model selector etc. */} + {header && ( +
+ {header} +
+ )} thread.isEmpty}> diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx index 1e0259447..34b2cc814 100644 --- a/surfsense_web/components/new-chat/chat-header.tsx +++ b/surfsense_web/components/new-chat/chat-header.tsx @@ -47,12 +47,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) { return ( <> - {/* Header Bar */} -
- -
- - {/* Config Sidebar */} + Date: Tue, 23 Dec 2025 18:50:24 -0800 Subject: [PATCH 6/7] chore: linting --- surfsense_web/components/assistant-ui/thread.tsx | 6 +----- surfsense_web/components/new-chat/model-selector.tsx | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index 6e69296e5..9596f4fe0 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -227,11 +227,7 @@ export const Thread: FC = ({ messageThinkingSteps = new Map(), head className="aui-thread-viewport relative flex flex-1 min-h-0 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4" > {/* Optional sticky header for model selector etc. */} - {header && ( -
- {header} -
- )} + {header &&
{header}
} thread.isEmpty}> diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index 2af476e3b..5dcf3bafa 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -211,7 +211,10 @@ export function ModelSelector({ onEdit, onAddNew, className }: ModelSelectorProp align="start" sideOffset={8} > - + {/* Switching overlay */} {isSwitching && (
From aeb9379c278fb440f5e1eb559b78047cd2f096fe Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Tue, 23 Dec 2025 19:10:58 -0800 Subject: [PATCH 7/7] feat: UX fixes --- surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx | 2 +- .../dashboard/[search_space_id]/connectors/(manage)/page.tsx | 2 +- .../app/dashboard/[search_space_id]/documents/(manage)/page.tsx | 2 +- .../app/dashboard/[search_space_id]/logs/(manage)/page.tsx | 2 +- .../app/dashboard/[search_space_id]/sources/add/page.tsx | 2 +- surfsense_web/components/assistant-ui/thread.tsx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index 284c1ebc0..808f941d6 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -253,7 +253,7 @@ export function DashboardClientLayout({ />
-
+
diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx index f107ffa6c..e2f219448 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx @@ -250,7 +250,7 @@ export default function ConnectorsPage() { }; return ( -
+
{/* Summary Dashboard */} +
{ return (
{/* Greeting positioned above the composer - fixed position */} -
+

{getTimeBasedGreeting(user?.email)}