mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-02 22:01:05 +02:00
display user avatar in shared chat messages
This commit is contained in:
parent
dcd36166fc
commit
83d6b735fa
4 changed files with 98 additions and 107 deletions
|
|
@ -411,13 +411,9 @@ async def get_thread_messages(
|
||||||
Requires CHATS_READ permission.
|
Requires CHATS_READ permission.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Get thread with messages and their authors
|
# Get thread first
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(NewChatThread)
|
select(NewChatThread).filter(NewChatThread.id == thread_id)
|
||||||
.options(
|
|
||||||
selectinload(NewChatThread.messages).selectinload(NewChatMessage.author)
|
|
||||||
)
|
|
||||||
.filter(NewChatThread.id == thread_id)
|
|
||||||
)
|
)
|
||||||
thread = result.scalars().first()
|
thread = result.scalars().first()
|
||||||
|
|
||||||
|
|
@ -436,6 +432,15 @@ async def get_thread_messages(
|
||||||
# Check thread-level access based on visibility
|
# Check thread-level access based on visibility
|
||||||
await check_thread_access(session, thread, user)
|
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
|
# Return messages in the format expected by assistant-ui
|
||||||
messages = [
|
messages = [
|
||||||
NewChatMessageRead(
|
NewChatMessageRead(
|
||||||
|
|
@ -448,7 +453,7 @@ async def get_thread_messages(
|
||||||
author_display_name=msg.author.display_name if msg.author else None,
|
author_display_name=msg.author.display_name if msg.author else None,
|
||||||
author_avatar_url=msg.author.avatar_url 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)
|
return ThreadHistoryLoadResponse(messages=messages)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
return {
|
||||||
id: `msg-${msg.id}`,
|
id: `msg-${msg.id}`,
|
||||||
role: msg.role,
|
role: msg.role,
|
||||||
content,
|
content,
|
||||||
createdAt: new Date(msg.created_at),
|
createdAt: new Date(msg.created_at),
|
||||||
attachments,
|
attachments,
|
||||||
|
metadata,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -306,12 +319,6 @@ export default function NewChatPage() {
|
||||||
if (steps.length > 0) {
|
if (steps.length > 0) {
|
||||||
restoredThinkingSteps.set(`msg-${msg.id}`, steps);
|
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") {
|
if (msg.role === "user") {
|
||||||
const docs = extractMentionedDocuments(msg.content);
|
const docs = extractMentionedDocuments(msg.content);
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,7 @@ import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
FileText,
|
|
||||||
Loader2,
|
Loader2,
|
||||||
PencilIcon,
|
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
SquareIcon,
|
SquareIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -31,7 +29,6 @@ import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
mentionedDocumentIdsAtom,
|
mentionedDocumentIdsAtom,
|
||||||
mentionedDocumentsAtom,
|
mentionedDocumentsAtom,
|
||||||
messageDocumentsMapAtom,
|
|
||||||
} from "@/atoms/chat/mentioned-documents.atom";
|
} from "@/atoms/chat/mentioned-documents.atom";
|
||||||
import {
|
import {
|
||||||
globalNewLLMConfigsAtom,
|
globalNewLLMConfigsAtom,
|
||||||
|
|
@ -42,8 +39,8 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
||||||
import {
|
import {
|
||||||
ComposerAddAttachment,
|
ComposerAddAttachment,
|
||||||
ComposerAttachments,
|
ComposerAttachments,
|
||||||
UserMessageAttachments,
|
|
||||||
} from "@/components/assistant-ui/attachment";
|
} from "@/components/assistant-ui/attachment";
|
||||||
|
import { UserMessage } from "@/components/assistant-ui/user-message";
|
||||||
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
||||||
import {
|
import {
|
||||||
InlineMentionEditor,
|
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 (
|
|
||||||
<MessagePrimitive.Root
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<div className="aui-user-message-content-wrapper col-start-2 min-w-0">
|
|
||||||
{/* Display attachments and mentioned documents */}
|
|
||||||
{(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && (
|
|
||||||
<div className="flex flex-wrap items-end gap-2 mb-2 justify-end">
|
|
||||||
{/* Attachments (images show as thumbnails, documents as chips) */}
|
|
||||||
<UserMessageAttachments />
|
|
||||||
{/* Mentioned documents as chips */}
|
|
||||||
{mentionedDocs?.map((doc) => (
|
|
||||||
<span
|
|
||||||
key={`${doc.document_type}:${doc.id}`}
|
|
||||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary border border-primary/20"
|
|
||||||
title={doc.title}
|
|
||||||
>
|
|
||||||
<FileText className="size-3" />
|
|
||||||
<span className="max-w-[150px] truncate">{doc.title}</span>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Message bubble with action bar positioned relative to it */}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
|
|
||||||
<MessagePrimitive.Parts />
|
|
||||||
</div>
|
|
||||||
<div className="aui-user-action-bar-wrapper absolute top-1/2 right-full -translate-y-1/2 pr-1">
|
|
||||||
<UserActionBar />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" />
|
|
||||||
</MessagePrimitive.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserActionBar: FC = () => {
|
|
||||||
return (
|
|
||||||
<ActionBarPrimitive.Root
|
|
||||||
hideWhenRunning
|
|
||||||
autohide="not-last"
|
|
||||||
className="aui-user-action-bar-root flex flex-col items-end"
|
|
||||||
>
|
|
||||||
<ActionBarPrimitive.Edit asChild>
|
|
||||||
<TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
|
|
||||||
<PencilIcon />
|
|
||||||
</TooltipIconButton>
|
|
||||||
</ActionBarPrimitive.Edit>
|
|
||||||
</ActionBarPrimitive.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EditComposer: FC = () => {
|
const EditComposer: FC = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,53 @@
|
||||||
import { ActionBarPrimitive, MessagePrimitive, useAssistantState } from "@assistant-ui/react";
|
import { ActionBarPrimitive, MessagePrimitive, useAssistantState } from "@assistant-ui/react";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
import { FileText, PencilIcon } from "lucide-react";
|
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 { messageDocumentsMapAtom } from "@/atoms/chat/mentioned-documents.atom";
|
||||||
import { UserMessageAttachments } from "@/components/assistant-ui/attachment";
|
import { UserMessageAttachments } from "@/components/assistant-ui/attachment";
|
||||||
import { BranchPicker } from "@/components/assistant-ui/branch-picker";
|
import { BranchPicker } from "@/components/assistant-ui/branch-picker";
|
||||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||||
|
|
||||||
|
interface AuthorMetadata {
|
||||||
|
displayName: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAvatar: FC<AuthorMetadata> = ({ 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 (
|
||||||
|
<img
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={displayName || "User"}
|
||||||
|
className="size-8 rounded-full object-cover"
|
||||||
|
onError={() => setHasError(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UserMessage: FC = () => {
|
export const UserMessage: FC = () => {
|
||||||
const messageId = useAssistantState(({ message }) => message?.id);
|
const messageId = useAssistantState(({ message }) => message?.id);
|
||||||
const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom);
|
const messageDocumentsMap = useAtomValue(messageDocumentsMapAtom);
|
||||||
const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined;
|
const mentionedDocs = messageId ? messageDocumentsMap[messageId] : undefined;
|
||||||
|
const metadata = useAssistantState(({ message }) => message?.metadata);
|
||||||
|
const author = metadata?.custom?.author as AuthorMetadata | undefined;
|
||||||
const hasAttachments = useAssistantState(
|
const hasAttachments = useAssistantState(
|
||||||
({ message }) => message?.attachments && message.attachments.length > 0
|
({ 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"
|
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"
|
data-role="user"
|
||||||
>
|
>
|
||||||
<div className="aui-user-message-content-wrapper col-start-2 min-w-0">
|
<div className="aui-user-message-content-wrapper col-start-2 min-w-0 flex items-end gap-2">
|
||||||
{/* Display attachments and mentioned documents */}
|
<div className="flex-1 min-w-0">
|
||||||
{(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && (
|
{/* Display attachments and mentioned documents */}
|
||||||
<div className="flex flex-wrap items-end gap-2 mb-2 justify-end">
|
{(hasAttachments || (mentionedDocs && mentionedDocs.length > 0)) && (
|
||||||
{/* Attachments (images show as thumbnails, documents as chips) */}
|
<div className="flex flex-wrap items-end gap-2 mb-2 justify-end">
|
||||||
<UserMessageAttachments />
|
{/* Attachments (images show as thumbnails, documents as chips) */}
|
||||||
{/* Mentioned documents as chips */}
|
<UserMessageAttachments />
|
||||||
{mentionedDocs?.map((doc) => (
|
{/* Mentioned documents as chips */}
|
||||||
<span
|
{mentionedDocs?.map((doc) => (
|
||||||
key={`${doc.document_type}:${doc.id}`}
|
<span
|
||||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary border border-primary/20"
|
key={`${doc.document_type}:${doc.id}`}
|
||||||
title={doc.title}
|
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-primary/10 text-xs font-medium text-primary border border-primary/20"
|
||||||
>
|
title={doc.title}
|
||||||
<FileText className="size-3" />
|
>
|
||||||
<span className="max-w-[150px] truncate">{doc.title}</span>
|
<FileText className="size-3" />
|
||||||
</span>
|
<span className="max-w-[150px] truncate">{doc.title}</span>
|
||||||
))}
|
</span>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
{/* Message bubble with action bar positioned relative to it */}
|
)}
|
||||||
<div className="relative">
|
{/* Message bubble with action bar positioned relative to it */}
|
||||||
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
|
<div className="relative">
|
||||||
<MessagePrimitive.Parts />
|
<div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
|
||||||
</div>
|
<MessagePrimitive.Parts />
|
||||||
<div className="aui-user-action-bar-wrapper absolute top-1/2 right-full -translate-y-1/2 pr-1">
|
</div>
|
||||||
<UserActionBar />
|
<div className="aui-user-action-bar-wrapper absolute top-1/2 right-full -translate-y-1/2 pr-1">
|
||||||
|
<UserActionBar />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* User avatar - only shown in shared chats */}
|
||||||
|
{author && (
|
||||||
|
<div className="shrink-0">
|
||||||
|
<UserAvatar displayName={author.displayName} avatarUrl={author.avatarUrl} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" />
|
<BranchPicker className="aui-user-branch-picker -mr-1 col-span-full col-start-1 row-start-3 justify-end" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue