diff --git a/frontend-web/src/components/workspace/messages/message-list-item.tsx b/frontend-web/src/components/workspace/messages/message-list-item.tsx index 4daa04ce..48ad91e0 100644 --- a/frontend-web/src/components/workspace/messages/message-list-item.tsx +++ b/frontend-web/src/components/workspace/messages/message-list-item.tsx @@ -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*`, "i").exec( + source, + ); + return match?.[1]?.trim() ?? ""; +} + +function parseEmbeddedPageContext(source: string): { + content: string; + pageContext: EmbeddedPageContext | null; +} { + const match = /\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 ? ( - - ) : 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 ? ( + + ) : 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 ? ( - - {desensitize(contentToDisplay)} - - ) : null; - return ( -
- {filesList} - {messageResponse && ( - - {messageResponse} - - )} + if (isHuman) { + const messageResponse = parsedHumanContent.content ? ( + + {desensitize(parsedHumanContent.content)} + + ) : null; + return ( +
+ {filesList} + {parsedHumanContent.pageContext ? ( + + ) : null} + {messageResponse && ( + + {messageResponse} + + )}
); @@ -331,7 +381,39 @@ function MessageContent_({ ) : null} ); -} +} + +function EmbeddedPageContextCard({ + pageContext, +}: { + pageContext: EmbeddedPageContext; +}) { + return ( +
+ {pageContext.title ? ( +
+ {pageContext.title} +
+ ) : null} + {pageContext.url ? ( + + {pageContext.url} + + ) : null} + {pageContext.content ? ( +
+ {pageContext.content} +
+ ) : null} +
+ ); +} /** * Format a wall-clock message time as ``YYYY-MM-DD HH:mm`` for display, plus a diff --git a/frontend-web/src/pages/IframeChatPage.tsx b/frontend-web/src/pages/IframeChatPage.tsx index ec046d50..78fede54 100644 --- a/frontend-web/src/pages/IframeChatPage.tsx +++ b/frontend-web/src/pages/IframeChatPage.tsx @@ -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(`\n${prompt}\n`); + } + + if (pageContext) { + const pageContextParts = [ + pageContext.title ? `\n${pageContext.title}\n` : null, + pageContext.url ? `\n${pageContext.url}\n` : null, + pageContext.content + ? `\n${pageContext.content}\n` + : null, + ].filter(Boolean); + + if (pageContextParts.length > 0) { + sections.push( + `\n${pageContextParts.join("\n\n")}\n`, + ); + } + } + + 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, + ) => { 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 = {}; + 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 ( -
+
-
+
{!isLandingState && ( - {children} + + {children} + @@ -146,7 +158,11 @@ export default function WorkspaceRoutes({ +