diff --git a/surfsense_backend/app/routes/new_chat_routes.py b/surfsense_backend/app/routes/new_chat_routes.py index 4ee515770..e4dc5714a 100644 --- a/surfsense_backend/app/routes/new_chat_routes.py +++ b/surfsense_backend/app/routes/new_chat_routes.py @@ -411,13 +411,9 @@ async def get_thread_messages( Requires CHATS_READ permission. """ try: - # Get thread with messages and their authors + # Get thread first result = await session.execute( - select(NewChatThread) - .options( - selectinload(NewChatThread.messages).selectinload(NewChatMessage.author) - ) - .filter(NewChatThread.id == thread_id) + select(NewChatThread).filter(NewChatThread.id == thread_id) ) thread = result.scalars().first() @@ -436,6 +432,15 @@ async def get_thread_messages( # Check thread-level access based on visibility await check_thread_access(session, thread, user) + # Get messages with their authors loaded + messages_result = await session.execute( + select(NewChatMessage) + .options(selectinload(NewChatMessage.author)) + .filter(NewChatMessage.thread_id == thread_id) + .order_by(NewChatMessage.created_at) + ) + db_messages = messages_result.scalars().all() + # Return messages in the format expected by assistant-ui messages = [ NewChatMessageRead( @@ -448,7 +453,7 @@ async def get_thread_messages( author_display_name=msg.author.display_name if msg.author else None, author_avatar_url=msg.author.avatar_url if msg.author else None, ) - for msg in thread.messages + for msg in db_messages ] return ThreadHistoryLoadResponse(messages=messages) 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 fbe4a9d95..6e4ab3039 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 @@ -185,12 +185,25 @@ function convertToThreadMessage(msg: MessageRecord): ThreadMessageLike { } } + // Build metadata.custom for author display in shared chats + const metadata = msg.author_id + ? { + custom: { + author: { + displayName: msg.author_display_name ?? null, + avatarUrl: msg.author_avatar_url ?? null, + }, + }, + } + : undefined; + return { id: `msg-${msg.id}`, role: msg.role, content, createdAt: new Date(msg.created_at), attachments, + metadata, }; } @@ -306,12 +319,6 @@ export default function NewChatPage() { if (steps.length > 0) { restoredThinkingSteps.set(`msg-${msg.id}`, steps); } - // Hydrate write_todos plan state from persisted tool calls - // Disabled for now - // const writeTodosCalls = extractWriteTodosFromContent(msg.content); - // for (const todoData of writeTodosCalls) { - // hydratePlanState(todoData); - // } } if (msg.role === "user") { const docs = extractMentionedDocuments(msg.content); diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index bf46e3d97..2507fb8a9 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -19,9 +19,7 @@ import { ChevronRightIcon, CopyIcon, DownloadIcon, - FileText, Loader2, - PencilIcon, RefreshCwIcon, SquareIcon, } from "lucide-react"; @@ -31,7 +29,6 @@ import { createPortal } from "react-dom"; import { mentionedDocumentIdsAtom, mentionedDocumentsAtom, - messageDocumentsMapAtom, } from "@/atoms/chat/mentioned-documents.atom"; import { globalNewLLMConfigsAtom, @@ -42,8 +39,8 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { ComposerAddAttachment, ComposerAttachments, - UserMessageAttachments, } from "@/components/assistant-ui/attachment"; +import { UserMessage } from "@/components/assistant-ui/user-message"; import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup"; import { InlineMentionEditor, @@ -639,69 +636,6 @@ const AssistantActionBar: FC = () => { ); }; -const UserMessage: FC = () => { - const messageId = useAssistantState(({ message }) => message?.id); - const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom); - const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined; - const hasAttachments = useAssistantState( - ({ message }) => message?.attachments && message.attachments.length > 0 - ); - - return ( - -
- {/* Display attachments and mentioned documents */} - {(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && ( -
- {/* Attachments (images show as thumbnails, documents as chips) */} - - {/* Mentioned documents as chips */} - {mentionedDocs?.map((doc) => ( - - - {doc.title} - - ))} -
- )} - {/* Message bubble with action bar positioned relative to it */} -
-
- -
-
- -
-
-
- - -
- ); -}; - -const UserActionBar: FC = () => { - return ( - - - - - - - - ); -}; const EditComposer: FC = () => { return ( diff --git a/surfsense_web/components/assistant-ui/user-message.tsx b/surfsense_web/components/assistant-ui/user-message.tsx index 745542304..ee23f6aab 100644 --- a/surfsense_web/components/assistant-ui/user-message.tsx +++ b/surfsense_web/components/assistant-ui/user-message.tsx @@ -1,16 +1,53 @@ import { ActionBarPrimitive, MessagePrimitive, useAssistantState } from "@assistant-ui/react"; import { useAtomValue } from "jotai"; import { FileText, PencilIcon } from "lucide-react"; -import type { FC } from "react"; +import { type FC, useState } from "react"; import { messageDocumentsMapAtom } from "@/atoms/chat/mentioned-documents.atom"; import { UserMessageAttachments } from "@/components/assistant-ui/attachment"; import { BranchPicker } from "@/components/assistant-ui/branch-picker"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +interface AuthorMetadata { + displayName: string | null; + avatarUrl: string | null; +} + +const UserAvatar: FC = ({ displayName, avatarUrl }) => { + const [hasError, setHasError] = useState(false); + + const initials = displayName + ? displayName + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2) + : "U"; + + if (avatarUrl && !hasError) { + return ( + {displayName setHasError(true)} + /> + ); + } + + return ( +
+ {initials} +
+ ); +}; + export const UserMessage: FC = () => { const messageId = useAssistantState(({ message }) => message?.id); const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom); const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined; + const metadata = useAssistantState(({ message }) => message?.metadata); + const author = metadata?.custom?.author as AuthorMetadata | undefined; const hasAttachments = useAssistantState( ({ message }) => message?.attachments && message.attachments.length > 0 ); @@ -20,34 +57,42 @@ export const UserMessage: FC = () => { className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2" data-role="user" > -
- {/* Display attachments and mentioned documents */} - {(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && ( -
- {/* Attachments (images show as thumbnails, documents as chips) */} - - {/* Mentioned documents as chips */} - {mentionedDocs?.map((doc) => ( - - - {doc.title} - - ))} -
- )} - {/* Message bubble with action bar positioned relative to it */} -
-
- -
-
- +
+
+ {/* Display attachments and mentioned documents */} + {(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && ( +
+ {/* Attachments (images show as thumbnails, documents as chips) */} + + {/* Mentioned documents as chips */} + {mentionedDocs?.map((doc) => ( + + + {doc.title} + + ))} +
+ )} + {/* Message bubble with action bar positioned relative to it */} +
+
+ +
+
+ +
+ {/* User avatar - only shown in shared chats */} + {author && ( +
+ +
+ )}