iframe 页面优化

This commit is contained in:
ZyGuo committed 2026-06-01 17:27:57 +08:00
1 parent 16adb90653
commit e6af567206
3 files changed
+231 -47

No files matched your search

@@ -45,13 +45,53 @@ import { humanMessagePlugins } from "@/core/streamdown";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
import { CitationLink } from "../citations/citation-link";
import { CopyButton } from "../copy-button";
import { SkillDisplayBlock } from "../skill-display-block";
import { ThreadContext } from "./context";
import { DownloadWordButton } from "./download-word-button";
import { MarkdownContent, type MarkdownContentProps } from "./markdown-content";
import { MessageTokenUsage } from "./message-token-usage";
import { CitationLink } from "../citations/citation-link";
import { CopyButton } from "../copy-button";
import { SkillDisplayBlock } from "../skill-display-block";
import { ThreadContext } from "./context";
import { DownloadWordButton } from "./download-word-button";
import { MarkdownContent, type MarkdownContentProps } from "./markdown-content";
import { MessageTokenUsage } from "./message-token-usage";
type EmbeddedPageContext = {
title?: string;
url?: string;
content?: string;
};
function extractTagContent(source: string, tagName: string) {
const match = new RegExp(`<${tagName}>\\s*([\\s\\S]*?)\\s*</${tagName}>`, "i").exec(
source,
);
return match?.[1]?.trim() ?? "";
}
function parseEmbeddedPageContext(source: string): {
content: string;
pageContext: EmbeddedPageContext | null;
} {
const match = /<page_context>\s*([\s\S]*?)\s*<\/page_context>/i.exec(source);
if (!match) {
return { content: source, pageContext: null };
}
const body = match[1] ?? "";
const pageContext = {
title: extractTagContent(body, "title") || undefined,
url: extractTagContent(body, "url") || undefined,
content: extractTagContent(body, "content") || undefined,
};
const cleanedContent = source
.replace(match[0], "")
.replace(/\n{3,}/g, "\n\n")
.trim();
if (!pageContext.title && !pageContext.url && !pageContext.content) {
return { content: cleanedContent, pageContext: null };
}
return { content: cleanedContent, pageContext };
}
/**
* Resolve an in-conversation file path to a fetchable URL.
@@ -165,7 +205,7 @@ function MessageImage({
);
}
function MessageContent_({
function MessageContent_({
className,
message,
isLoading = false,
@@ -239,17 +279,24 @@ function MessageContent_({
return files as FileInMessage[];
}, [message.additional_kwargs?.files, rawContent]);
const contentToDisplay = useMemo(() => {
if (isHuman) {
return rawContent ? stripArtifactTag(stripUploadedFilesTag(rawContent)) : "";
}
return applyInlineCitations(rawContent ?? "", displaySources);
}, [rawContent, isHuman, displaySources]);
const filesList =
files && files.length > 0 ? (
<RichFilesList files={files} threadId={threadId} />
) : null;
const contentToDisplay = useMemo(() => {
if (isHuman) {
return rawContent ? stripArtifactTag(stripUploadedFilesTag(rawContent)) : "";
}
return applyInlineCitations(rawContent ?? "", displaySources);
}, [rawContent, isHuman, displaySources]);
const parsedHumanContent = useMemo(
() =>
isHuman
? parseEmbeddedPageContext(contentToDisplay)
: { content: contentToDisplay, pageContext: null },
[contentToDisplay, isHuman],
);
const filesList =
files && files.length > 0 ? (
<RichFilesList files={files} threadId={threadId} />
) : null;
// Uploading state: mock AI message shown while files upload
if (message.additional_kwargs?.element === "task") {
@@ -284,25 +331,28 @@ function MessageContent_({
);
}
if (isHuman) {
const messageResponse = contentToDisplay ? (
<AIElementMessageResponse
remarkPlugins={humanMessagePlugins.remarkPlugins}
rehypePlugins={humanMessagePlugins.rehypePlugins}
components={components}
parseIncompleteMarkdown={false}
>
{desensitize(contentToDisplay)}
</AIElementMessageResponse>
) : null;
return (
<div className={cn("ml-auto flex flex-col gap-2", className)}>
{filesList}
{messageResponse && (
<AIElementMessageContent className="w-fit">
{messageResponse}
</AIElementMessageContent>
)}
if (isHuman) {
const messageResponse = parsedHumanContent.content ? (
<AIElementMessageResponse
remarkPlugins={humanMessagePlugins.remarkPlugins}
rehypePlugins={humanMessagePlugins.rehypePlugins}
components={components}
parseIncompleteMarkdown={false}
>
{desensitize(parsedHumanContent.content)}
</AIElementMessageResponse>
) : null;
return (
<div className={cn("ml-auto flex flex-col gap-2", className)}>
{filesList}
{parsedHumanContent.pageContext ? (
<EmbeddedPageContextCard pageContext={parsedHumanContent.pageContext} />
) : null}
{messageResponse && (
<AIElementMessageContent className="w-fit">
{messageResponse}
</AIElementMessageContent>
)}
<MessageTimeStamp time={messageTime} align="right" />
</div>
);
@@ -331,7 +381,39 @@ function MessageContent_({
) : null}
</AIElementMessageContent>
);
}
}
function EmbeddedPageContextCard({
pageContext,
}: {
pageContext: EmbeddedPageContext;
}) {
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">
{pageContext.title ? (
<div className="truncate font-medium" title={pageContext.title}>
{pageContext.title}
</div>
) : null}
{pageContext.url ? (
<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>
);
}
/**
* Format a wall-clock message time as ``YYYY-MM-DD HH:mm`` for display, plus a
+91 -5
View File
@@ -38,10 +38,57 @@ type BrainyChatSubmitMessage = {
type?: string;
payload?: {
text?: string;
prompt?: string;
pageContext?: {
title?: string;
content?: string;
url?: string;
};
};
requestId?: string;
};
function buildIframeMessageText({
text,
prompt,
pageContext,
}: {
text: string;
prompt: string;
pageContext?:
| {
title?: string;
content?: string;
url?: string;
}
| undefined;
}) {
const primaryText = text || prompt;
const sections = [primaryText];
if (prompt && prompt !== primaryText) {
sections.push(`<extension_prompt>\n${prompt}\n</extension_prompt>`);
}
if (pageContext) {
const pageContextParts = [
pageContext.title ? `<title>\n${pageContext.title}\n</title>` : null,
pageContext.url ? `<url>\n${pageContext.url}\n</url>` : null,
pageContext.content
? `<content>\n${pageContext.content}\n</content>`
: null,
].filter(Boolean);
if (pageContextParts.length > 0) {
sections.push(
`<page_context>\n${pageContextParts.join("\n\n")}\n</page_context>`,
);
}
}
return sections.join("\n\n");
}
export default function IframeChatPage() {
const routeBase = useRouteBase();
const router = useRouter();
@@ -116,7 +163,10 @@ export default function IframeChatPage() {
}, [selectedArtifact, thread.values.artifacts]);
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
(
message: PromptInputMessage,
externalContext?: Record<string, unknown>,
) => {
if (
selectedAgentId &&
isNewThread &&
@@ -152,6 +202,9 @@ export default function IframeChatPage() {
if (settings.context.writing_mode && writingArtifactPath) {
extraContext.writing_artifact_path = writingArtifactPath;
}
if (externalContext) {
Object.assign(extraContext, externalContext);
}
void sendMessage(
threadId,
message,
@@ -194,9 +247,42 @@ export default function IframeChatPage() {
if (data?.type !== "brainy:chat-submit") return;
const text = data.payload?.text?.trim() ?? "";
if (!text) return;
const prompt = data.payload?.prompt?.trim() ?? "";
const rawPageContext = data.payload?.pageContext;
const pageContext = rawPageContext
? {
...(rawPageContext.title?.trim()
? { title: rawPageContext.title.trim() }
: {}),
...(rawPageContext.content?.trim()
? { content: rawPageContext.content.trim() }
: {}),
...(rawPageContext.url?.trim()
? { url: rawPageContext.url.trim() }
: {}),
}
: undefined;
const hasPageContext =
!!pageContext &&
Object.values(pageContext).some((value) => value.length > 0);
const submitText = buildIframeMessageText({ text, prompt, pageContext });
console.log("收到插件消息", {
text,
prompt,
pageContext,
requestId: data.requestId,
});
if (!submitText) return;
handleSubmit({ text, files: [] });
const iframeContext: Record<string, unknown> = {};
if (data.requestId) {
iframeContext.brainy_request_id = data.requestId;
}
handleSubmit(
{ text: submitText, files: [] },
Object.keys(iframeContext).length > 0 ? iframeContext : undefined,
);
};
window.addEventListener("message", handleMessage);
@@ -224,7 +310,7 @@ export default function IframeChatPage() {
return (
<ThreadContext.Provider value={{ thread, isMock }}>
<ChatBox threadId={threadId} disableArtifacts>
<div className="relative flex size-full min-h-0 justify-between">
<div className="relative flex size-full min-h-0 justify-between overflow-hidden">
<header
className={cn(
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center border-b px-4",
@@ -244,7 +330,7 @@ export default function IframeChatPage() {
<ExportTrigger threadId={threadId} />
</div>
</header>
<main className="flex min-h-0 max-w-full grow flex-col">
<main className="flex min-h-0 max-w-full grow flex-col overflow-hidden">
<div className="flex size-full justify-center">
{!isLandingState && (
<MessageList
+19 -3
View File
@@ -41,6 +41,7 @@ import StudioNotebooksPage from "./StudioNotebooksPage";
import StudioNotebookDetailPage from "./StudioNotebookDetailPage";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import "@/roundtable-planning/styles/roundtable-planning.css";
import { cn } from "@/lib/utils";
export function WorkspaceLayout({
children,
@@ -48,12 +49,16 @@ export function WorkspaceLayout({
pageSidebarHidden = false,
headerHidden = false,
compactWorkspaceSidebar = false,
wrapperOverflowHidden = false,
insetOverflowHidden = false,
}: {
children: ReactNode;
pageSidebarCollapsed: boolean;
pageSidebarHidden?: boolean;
headerHidden?: boolean;
compactWorkspaceSidebar?: boolean;
wrapperOverflowHidden?: boolean;
insetOverflowHidden?: boolean;
}) {
const pageSidebarWidth = pageSidebarHidden
? "0rem"
@@ -63,7 +68,10 @@ export function WorkspaceLayout({
return (
<SidebarProvider
className="h-full w-full overflow-x-hidden [&_[data-slot=sidebar-container]]:left-[var(--page-sidebar-width)]"
className={[
"h-full w-full [&_[data-slot=sidebar-container]]:left-[var(--page-sidebar-width)]",
wrapperOverflowHidden ? "overflow-hidden" : "overflow-x-hidden",
].join(" ")}
style={
{
"--page-sidebar-width": pageSidebarWidth,
@@ -76,7 +84,11 @@ export function WorkspaceLayout({
defaultOpen
>
<WorkspaceSidebar compactChrome={compactWorkspaceSidebar} />
<SidebarInset className="min-w-0">{children}</SidebarInset>
<SidebarInset
className={cn("min-w-0", insetOverflowHidden && "overflow-hidden")}
>
{children}
</SidebarInset>
<CommandPalette />
<Toaster position="top-center" />
</SidebarProvider>
@@ -146,7 +158,11 @@ export default function WorkspaceRoutes({
<Route
path="chats_iframe/:thread_id"
element={
<WorkspaceLayout {...layoutProps}>
<WorkspaceLayout
{...layoutProps}
wrapperOverflowHidden
insetOverflowHidden
>
<ChatRuntime>
<IframeChatPage />
</ChatRuntime>