iframe 页面优化
This commit is contained in:
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user