fix: 对话区域显示问题修复
This commit is contained in:
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}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
Reference in new issue
Block a user