fix: 对话区域显示问题修复

This commit is contained in:
ZyGuo committed 2026-06-02 11:26:28 +08:00
1 parent 2e6eff43ad
commit 49cfc687f9
4 files changed
+126 -82

No files matched your search

@@ -1,13 +1,13 @@
import type { Message } from "@langchain/langgraph-sdk"; import type { Message } from "@langchain/langgraph-sdk";
import { ClockIcon, FileIcon, Loader2Icon } from "lucide-react"; import { ClockIcon, FileIcon, Loader2Icon } from "lucide-react";
import { import {
memo, memo,
useCallback, useCallback,
useContext, useContext,
useMemo, useMemo,
type AnchorHTMLAttributes, type AnchorHTMLAttributes,
type ImgHTMLAttributes, type ImgHTMLAttributes,
} from "react"; } from "react";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import { Loader } from "@/components/ai-elements/loader"; import { Loader } from "@/components/ai-elements/loader";
@@ -116,19 +116,21 @@ function useArtifactResolver(threadId: string): (path: string) => string {
); );
} }
export function MessageListItem({ export function MessageListItem({
className, className,
message, fitRowToContent = false,
isLoading, message,
threadId, isLoading,
tokenUsageEnabled = false, threadId,
tokenUsageEnabled = false,
displaySources = [], displaySources = [],
extraRehypePlugins, extraRehypePlugins,
}: { }: {
className?: string; className?: string;
message: Message; fitRowToContent?: boolean;
isLoading?: boolean; message: Message;
threadId: string; isLoading?: boolean;
threadId: string;
tokenUsageEnabled?: boolean; tokenUsageEnabled?: boolean;
displaySources?: RuntimeDisplaySource[]; displaySources?: RuntimeDisplaySource[];
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>; extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
@@ -138,11 +140,16 @@ export function MessageListItem({
extractContentFromMessage(message) ?? extractContentFromMessage(message) ??
extractReasoningContentFromMessage(message) ?? extractReasoningContentFromMessage(message) ??
""; "";
return ( return (
<AIElementMessage <AIElementMessage
className={cn("group/conversation-message relative w-full", className)} className={cn(
from={isHuman ? "user" : "assistant"} "group/conversation-message relative",
> fitRowToContent ? "max-w-full w-fit" : "w-full",
isHuman && fitRowToContent && "ml-auto",
className,
)}
from={isHuman ? "user" : "assistant"}
>
<MessageContent <MessageContent
className={isHuman ? "w-fit" : "w-full"} className={isHuman ? "w-fit" : "w-full"}
message={message} message={message}
@@ -343,20 +350,20 @@ function MessageContent_({
</AIElementMessageResponse> </AIElementMessageResponse>
) : null; ) : null;
return ( return (
<div className={cn("ml-auto flex flex-col gap-2", className)}> <div className={cn("ml-auto flex max-w-full min-w-0 flex-col items-end gap-2", className)}>
{filesList} {filesList}
{parsedHumanContent.pageContext ? (
<EmbeddedPageContextCard pageContext={parsedHumanContent.pageContext} />
) : null}
{messageResponse && ( {messageResponse && (
<AIElementMessageContent className="w-fit"> <AIElementMessageContent className="min-w-0 max-w-full w-fit">
{messageResponse} {messageResponse}
</AIElementMessageContent> </AIElementMessageContent>
)} )}
<MessageTimeStamp time={messageTime} align="right" /> {parsedHumanContent.pageContext ? (
</div> <EmbeddedPageContextCard pageContext={parsedHumanContent.pageContext} />
); ) : null}
} <MessageTimeStamp time={messageTime} align="right" />
</div>
);
}
return ( return (
<AIElementMessageContent className={className}> <AIElementMessageContent className={className}>
@@ -388,33 +395,22 @@ function EmbeddedPageContextCard({
}: { }: {
pageContext: EmbeddedPageContext; pageContext: EmbeddedPageContext;
}) { }) {
const previewText = [pageContext.title, pageContext.url, pageContext.content]
.filter(Boolean)
.join(" | ");
return ( return (
<div className="bg-muted/70 ml-auto flex max-w-sm flex-col gap-2 rounded-2xl border px-3 py-2 text-sm shadow-sm"> <div className="bg-muted/70 ml-auto flex w-fit min-w-0 max-w-[min(24rem,calc(100vw-2rem),100%)] flex-col gap-2 rounded-2xl border px-3 py-2 text-sm shadow-sm">
{pageContext.title ? ( <div
<div className="truncate font-medium" title={pageContext.title}> className="text-muted-foreground line-clamp-2 min-w-0 [overflow-wrap:anywhere] break-words text-xs"
{pageContext.title} title={previewText}
</div> >
) : null} {previewText}
{pageContext.url ? ( </div>
<a
className="text-muted-foreground block truncate text-xs underline underline-offset-2"
href={pageContext.url}
target="_blank"
rel="noopener noreferrer"
title={pageContext.url}
>
{pageContext.url}
</a>
) : null}
{pageContext.content ? (
<div className="text-muted-foreground truncate text-xs" title={pageContext.content}>
{pageContext.content}
</div>
) : null}
</div> </div>
); );
} }
/** /**
* Format a wall-clock message time as ``YYYY-MM-DD HH:mm`` for display, plus a * Format a wall-clock message time as ``YYYY-MM-DD HH:mm`` for display, plus a
* full localized string for the hover title. Returns ``null`` when no/invalid * full localized string for the hover title. Returns ``null`` when no/invalid
@@ -506,7 +502,7 @@ function isImageFile(filename: string): boolean {
* Format bytes to human-readable size string * Format bytes to human-readable size string
*/ */
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes === 0) return "—"; if (bytes === 0) return "0 B";
const kb = bytes / 1024; const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(1)} KB`; if (kb < 1024) return `${kb.toFixed(1)} KB`;
return `${(kb / 1024).toFixed(1)} MB`; return `${(kb / 1024).toFixed(1)} MB`;
@@ -795,3 +791,4 @@ function groupDisplaySources(sources: RuntimeDisplaySource[]) {
function normalizeName(name: string) { function normalizeName(name: string) {
return name.toLowerCase().replace(/[^a-z0-9]+/g, ""); return name.toLowerCase().replace(/[^a-z0-9]+/g, "");
} }
@@ -50,22 +50,26 @@ import { SubtaskCard } from "./subtask-card";
export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160; export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 160;
export const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 80; export const MESSAGE_LIST_FOLLOWUPS_EXTRA_PADDING_BOTTOM = 80;
export function MessageList({ export function MessageList({
className, className,
threadId, contentClassName,
thread, fitMessageRowsToContent = false,
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, threadId,
tokenUsageEnabled = false, thread,
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
tokenUsageEnabled = false,
extraRehypePlugins, extraRehypePlugins,
onClarificationSubmit, onClarificationSubmit,
onSetupAgentApprove, onSetupAgentApprove,
onSetupAgentReject, onSetupAgentReject,
}: { }: {
className?: string; className?: string;
threadId: string; contentClassName?: string;
thread: BaseStream<AgentThreadState>; fitMessageRowsToContent?: boolean;
paddingBottom?: number; threadId: string;
tokenUsageEnabled?: boolean; thread: BaseStream<AgentThreadState>;
paddingBottom?: number;
tokenUsageEnabled?: boolean;
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>; extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
onClarificationSubmit?: (answer: string) => void | Promise<void>; onClarificationSubmit?: (answer: string) => void | Promise<void>;
onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>; onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
@@ -213,12 +217,17 @@ export function MessageList({
if (thread.isThreadLoading && messages.length === 0) { if (thread.isThreadLoading && messages.length === 0) {
return <MessageListSkeleton />; return <MessageListSkeleton />;
} }
return ( return (
<Conversation <Conversation
className={cn("flex size-full flex-col justify-center", className)} className={cn("flex size-full flex-col justify-center", className)}
> >
<ConversationContent className="mx-auto w-full max-w-(--container-width-lg) gap-8 pt-12"> <ConversationContent
<MessageMetricsProvider messages={messages} isLoading={thread.isLoading}> className={cn(
"mx-auto w-full max-w-(--container-width-lg) gap-8 pt-12",
contentClassName,
)}
>
<MessageMetricsProvider messages={messages} isLoading={thread.isLoading}>
{groupMessages(messages, (group) => { {groupMessages(messages, (group) => {
if (group.type === "human" || group.type === "assistant") { if (group.type === "human" || group.type === "assistant") {
return group.messages.map((msg) => { return group.messages.map((msg) => {
@@ -233,11 +242,12 @@ export function MessageList({
}) })
: []; : [];
return ( return (
<MessageListItem <MessageListItem
key={`${group.id}/${msg.id}`} key={`${group.id}/${msg.id}`}
message={msg} fitRowToContent={fitMessageRowsToContent}
isLoading={thread.isLoading} message={msg}
threadId={threadId} isLoading={thread.isLoading}
threadId={threadId}
tokenUsageEnabled={tokenUsageEnabled} tokenUsageEnabled={tokenUsageEnabled}
displaySources={displaySources} displaySources={displaySources}
extraRehypePlugins={extraRehypePlugins} extraRehypePlugins={extraRehypePlugins}
+3 -2
View File
@@ -313,10 +313,11 @@ export default function IframeChatPage() {
</div> </div>
</header> </header>
<main className="flex min-h-0 max-w-full grow flex-col overflow-hidden"> <main className="flex min-h-0 max-w-full grow flex-col overflow-hidden">
<div className="flex size-full justify-center"> <div className="flex size-full min-w-0">
{!isLandingState && ( {!isLandingState && (
<MessageList <MessageList
className={cn("size-full", "pt-10")} className={cn("size-full", "pt-10")}
contentClassName="mx-0 max-w-full px-3 sm:px-4"
threadId={threadId} threadId={threadId}
thread={thread} thread={thread}
paddingBottom={messageListPaddingBottom} paddingBottom={messageListPaddingBottom}
@@ -332,7 +333,7 @@ export default function IframeChatPage() {
isLandingState && "-translate-y-[calc(50vh-96px)]", isLandingState && "-translate-y-[calc(50vh-96px)]",
isLandingState isLandingState
? "max-w-(--container-width-md)" ? "max-w-(--container-width-md)"
: "max-w-(--container-width-lg)", : "max-w-full",
)} )}
> >
<div className="absolute -top-4 right-0 left-0 z-0"> <div className="absolute -top-4 right-0 left-0 z-0">
@@ -43,6 +43,14 @@ import RoundtablePlanningPage from '@/roundtable-planning/pages/RoundtablePlanni
import '@/roundtable-planning/styles/roundtable-planning.css'; import '@/roundtable-planning/styles/roundtable-planning.css';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
function isInIframe(): boolean {
try {
return window.self !== window.top;
} catch {
return true;
}
}
export function WorkspaceLayout({ export function WorkspaceLayout({
children, children,
pageSidebarCollapsed, pageSidebarCollapsed,
@@ -116,6 +124,30 @@ export function ChatRuntime({ children }: { children: ReactNode }) {
); );
} }
function NewChatEntryRoute({
layoutProps,
}: {
layoutProps: {
pageSidebarCollapsed: boolean;
pageSidebarHidden: boolean;
headerHidden: boolean;
compactWorkspaceSidebar: boolean;
pageSidebarDark: boolean;
};
}) {
if (isInIframe()) {
return <Navigate to="/page/workspace/chats_iframe/new" replace />;
}
return (
<WorkspaceLayout {...layoutProps}>
<ChatRuntime>
<ChatPage />
</ChatRuntime>
</WorkspaceLayout>
);
}
export default function WorkspaceRoutes({ export default function WorkspaceRoutes({
pageSidebarCollapsed = false, pageSidebarCollapsed = false,
pageSidebarHidden = false, pageSidebarHidden = false,
@@ -149,6 +181,10 @@ export default function WorkspaceRoutes({
</WorkspaceLayout> </WorkspaceLayout>
} }
/> />
<Route
path="chats/new"
element={<NewChatEntryRoute layoutProps={layoutProps} />}
/>
<Route <Route
path="chats/:thread_id" path="chats/:thread_id"
element={ element={