diff --git a/frontend/package.json b/frontend/package.json index 95207d35..5b6a144e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,6 +63,7 @@ "codemirror": "^6.0.2", "date-fns": "^4.1.0", "docx": "^9.6.1", + "docx-preview": "^0.3.7", "dotenv": "^17.2.3", "embla-carousel-react": "^8.6.0", "gsap": "^3.13.0", @@ -79,6 +80,7 @@ "nextra-theme-docs": "^4.6.1", "nuxt-og-image": "^5.1.13", "ogl": "^1.0.11", + "pptx-preview": "^1.0.7", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.4.1", @@ -94,6 +96,7 @@ "unist-util-visit": "^5.0.0", "use-stick-to-bottom": "^1.1.1", "uuid": "^13.0.0", + "xlsx": "^0.18.5", "zod": "^3.24.2" }, "devDependencies": { diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fdf44013..63ab0766 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: docx: specifier: ^9.6.1 version: 9.6.1 + docx-preview: + specifier: ^0.3.7 + version: 0.3.7 dotenv: specifier: ^17.2.3 version: 17.2.4 @@ -185,6 +188,9 @@ importers: ogl: specifier: ^1.0.11 version: 1.0.11 + pptx-preview: + specifier: ^1.0.7 + version: 1.0.7 react: specifier: ^19.0.0 version: 19.2.4 @@ -230,6 +236,9 @@ importers: uuid: specifier: ^13.0.0 version: 13.0.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zod: specifier: ^3.24.2 version: 3.25.76 @@ -2548,6 +2557,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + ai@6.0.78: resolution: {integrity: sha512-eriIX/NLWfWNDeE/OJy8wmIp9fyaH7gnxTOCPT5bp0MNkvORstp1TwRUql9au8XjXzH7o2WApqbwgxJDDV0Rbw==} engines: {node: '>=18'} @@ -2803,6 +2816,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2875,6 +2892,10 @@ packages: codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} @@ -2934,6 +2955,11 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -3206,6 +3232,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + docx-preview@0.3.7: + resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==} + docx@9.6.1: resolution: {integrity: sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==} engines: {node: '>=10'} @@ -3221,6 +3250,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + echarts@5.6.0: + resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==} + embla-carousel-react@8.6.0: resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} peerDependencies: @@ -3553,6 +3585,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + framer-motion@12.34.0: resolution: {integrity: sha512-+/H49owhzkzQyxtn7nZeF4kdH++I2FWrESQ184Zbcw5cEqNHYkE5yxWxcTLSj5lNx3NWdbIRy5FHqUvetD8FWg==} peerDependencies: @@ -4190,6 +4226,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -4818,6 +4857,9 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + pptx-preview@1.0.7: + resolution: {integrity: sha512-YByocJuyxAR4YB4Q3+VAxdLfEvA5LojG1gAJsx2Mw0QU5FJPps/2fkJOupJ6oBbA+KdWRpuAk6G6T34rKCHVxw==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5274,6 +5316,10 @@ packages: resolution: {integrity: sha512-SBMgkuJYvP4F62daRfBNwYC2nXTEhNXAfsBZ/BB7Ly85/KnbnjmKM7/45ZrFbH6jIMiAliDUDPSZFUuXDvcg6A==} hasBin: true + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -5449,6 +5495,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -5800,10 +5849,23 @@ packages: wicked-good-xpath@1.3.0: resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true @@ -5836,6 +5898,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zrender@5.6.1: + resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==} + zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -8234,6 +8299,8 @@ snapshots: acorn@8.15.0: {} + adler-32@1.3.1: {} + ai@6.0.78(zod@3.25.76): dependencies: '@ai-sdk/gateway': 3.0.39(zod@3.25.76) @@ -8476,6 +8543,11 @@ snapshots: ccount@2.0.1: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -8564,6 +8636,8 @@ snapshots: '@codemirror/state': 6.5.4 '@codemirror/view': 6.39.13 + codepage@1.15.0: {} + collapse-white-space@2.1.0: {} color-convert@2.0.1: @@ -8609,6 +8683,8 @@ snapshots: dependencies: layout-base: 2.0.1 + crc-32@1.2.2: {} + crelt@1.0.6: {} cross-spawn@7.0.6: @@ -8899,6 +8975,10 @@ snapshots: dependencies: esutils: 2.0.3 + docx-preview@0.3.7: + dependencies: + jszip: 3.10.1 + docx@9.6.1: dependencies: '@types/node': 25.5.0 @@ -8920,6 +9000,11 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + echarts@5.6.0: + dependencies: + tslib: 2.3.0 + zrender: 5.6.1 + embla-carousel-react@8.6.0(react@19.2.4): dependencies: embla-carousel: 8.6.0 @@ -9449,6 +9534,8 @@ snapshots: format@0.2.2: {} + frac@1.1.2: {} + framer-motion@12.34.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: motion-dom: 12.34.0 @@ -10154,6 +10241,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.18.1: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -11179,6 +11268,14 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + pptx-preview@1.0.7: + dependencies: + echarts: 5.6.0 + jszip: 3.10.1 + lodash: 4.18.1 + tslib: 2.8.1 + uuid: 10.0.0 + prelude-ls@1.2.1: {} prettier-plugin-tailwindcss@0.6.14(prettier@3.8.1): @@ -11771,6 +11868,10 @@ snapshots: commander: 13.1.0 wicked-good-xpath: 1.3.0 + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stable-hash@0.0.5: {} stackblur-canvas@2.7.0: @@ -11966,6 +12067,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.3.0: {} + tslib@2.8.1: {} tw-animate-css@1.4.0: {} @@ -12335,8 +12438,22 @@ snapshots: wicked-good-xpath@1.3.0: {} + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xml-js@1.6.11: dependencies: sax: 1.6.0 @@ -12357,6 +12474,10 @@ snapshots: zod@4.3.6: {} + zrender@5.6.1: + dependencies: + tslib: 2.3.0 + zustand@4.5.7(@types/react@19.2.13)(react@19.2.4): dependencies: use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/frontend/src/components/ai-elements/chain-of-thought.tsx b/frontend/src/components/ai-elements/chain-of-thought.tsx index cd3a6b7a..8fda3db0 100644 --- a/frontend/src/components/ai-elements/chain-of-thought.tsx +++ b/frontend/src/components/ai-elements/chain-of-thought.tsx @@ -116,6 +116,7 @@ export const ChainOfThoughtHeader = memo( export type ChainOfThoughtStepProps = ComponentProps<"div"> & { icon?: LucideIcon | React.ReactElement; label: ReactNode; + action?: ReactNode; description?: ReactNode; status?: "complete" | "active" | "pending"; }; @@ -125,6 +126,7 @@ export const ChainOfThoughtStep = memo( className, icon: Icon = DotIcon, label, + action, description, status = "complete", children, @@ -151,7 +153,10 @@ export const ChainOfThoughtStep = memo(
-
{label}
+
+
{label}
+ {action &&
{action}
} +
{description && (
{description}
)} diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 388e32cc..47577084 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -8,12 +8,15 @@ import { useCallback, useEffect, useMemo, + useRef, useState, + type CSSProperties, type ComponentProps, type HTMLAttributes, } from "react"; import { toast } from "sonner"; import { Streamdown } from "streamdown"; +import * as XLSX from "xlsx"; import { Artifact, @@ -489,13 +492,22 @@ export function ArtifactFileDetail({
)} {!isCodeFile && ( - + isOfficePreviewKind(artifactPreviewKind) ? ( + + ) : ( + + ) )} @@ -522,7 +534,7 @@ export function ArtifactFilePreview({ return (
(null); + const [xlsxHtml, setXlsxHtml] = useState(""); + const [sheetNames, setSheetNames] = useState([]); + const [activeSheet, setActiveSheet] = useState(""); + const docxContainerRef = useRef(null); + const pptxContainerRef = useRef(null); + const workbookRef = useRef(null); + + const canRenderDocx = kind === "docx"; + const canRenderXlsx = kind === "xlsx"; + const canRenderPptx = kind === "pptx"; + + useEffect(() => { + let disposed = false; + + async function renderDocx() { + if (!canRenderDocx || !artifactUrl || !docxContainerRef.current) { + return; + } + setIsLoading(true); + setError(null); + try { + const response = await fetch(artifactUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const blob = await response.blob(); + const { renderAsync } = await import("docx-preview"); + if (disposed || !docxContainerRef.current) { + return; + } + docxContainerRef.current.innerHTML = ""; + await renderAsync(blob, docxContainerRef.current, undefined, { + ignoreWidth: false, + ignoreHeight: false, + breakPages: true, + inWrapper: true, + }); + } catch (err) { + console.error("Failed to render docx preview:", err); + if (!disposed) { + setError("无法预览该 DOCX 文件。"); + } + } finally { + if (!disposed) { + setIsLoading(false); + } + } + } + + void renderDocx(); + return () => { + disposed = true; + }; + }, [artifactUrl, canRenderDocx]); + + useEffect(() => { + let disposed = false; + + async function parseXlsx() { + if (!canRenderXlsx || !artifactUrl) { + return; + } + setIsLoading(true); + setError(null); + workbookRef.current = null; + try { + const response = await fetch(artifactUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const bytes = await response.arrayBuffer(); + const workbook = XLSX.read(bytes, { type: "array" }); + workbookRef.current = workbook; + const names = workbook.SheetNames ?? []; + if (names.length === 0) { + throw new Error("Empty workbook"); + } + if (disposed) return; + setSheetNames(names); + const first = names[0] ?? ""; + setActiveSheet(first); + const sheet = workbook.Sheets[first]; + const html = sheet + ? XLSX.utils.sheet_to_html(sheet, { id: "artifact-xlsx-preview" }) + : ""; + setXlsxHtml(html); + } catch (err) { + console.error("Failed to render xlsx preview:", err); + if (!disposed) { + setError("无法预览该 Excel 文件。"); + } + } finally { + if (!disposed) { + setIsLoading(false); + } + } + } + + void parseXlsx(); + return () => { + disposed = true; + }; + }, [artifactUrl, canRenderXlsx]); + + useEffect(() => { + if (!canRenderXlsx || !activeSheet || !workbookRef.current) { + return; + } + try { + const sheet = workbookRef.current.Sheets[activeSheet]; + if (!sheet) return; + setXlsxHtml(XLSX.utils.sheet_to_html(sheet, { id: "artifact-xlsx-preview" })); + } catch (err) { + console.error("Failed to switch xlsx sheet:", err); + setError("切换工作表失败。"); + } + }, [activeSheet, canRenderXlsx]); + useEffect(() => { + let disposed = false; + + type PptxPreviewModule = { + init: ( + container: HTMLElement, + options: { width: number; height: number }, + ) => { + preview: (buffer: ArrayBuffer) => Promise | void; + }; + }; + + async function renderPptx() { + if (!canRenderPptx || !artifactUrl || !pptxContainerRef.current) { + return; + } + setIsLoading(true); + setError(null); + try { + const response = await fetch(artifactUrl); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const bytes = await response.arrayBuffer(); + const pptxModule = (await import("pptx-preview")) as unknown as PptxPreviewModule; + if (disposed || !pptxContainerRef.current) { + return; + } + const container = pptxContainerRef.current; + container.innerHTML = ""; + const previewer = pptxModule.init(container, { width: 960, height: 540 }); + await Promise.resolve(previewer.preview(bytes)); + } catch (err) { + console.error("Failed to render pptx preview:", err); + if (!disposed) { + setError("无法预览该 PPT 文件。"); + } + } finally { + if (!disposed) { + setIsLoading(false); + } + } + } + + void renderPptx(); + return () => { + disposed = true; + }; + }, [artifactUrl, canRenderPptx]); + + return ( +
+ {canRenderXlsx && sheetNames.length > 0 && ( +
+ {sheetNames.map((sheetName) => ( + + ))} +
+ )} + +
+ {canRenderDocx && ( +
+ )} + {canRenderXlsx && xlsxHtml && ( +
+ )} + {canRenderPptx && ( +
+ )} +
+ + {error && ( + + )} + {isLoading && ( +
+ +
+ )} +
+ ); +} + +function ArtifactPreviewFallback({ + message, + fileName, + artifactUrl, +}: { + message: string; + fileName: string; + artifactUrl: string; +}) { + return ( +
+

{fileName}

+

{message}

+ + 在新标签页打开 + +
+ ); +} + function rewriteArtifactImagePaths(content: string, threadId?: string) { if (!threadId || !/\/?mnt\/user-data\//.test(content)) { return content; @@ -626,6 +902,9 @@ type ArtifactPreviewKind = | "video" | "audio" | "pdf" + | "docx" + | "xlsx" + | "pptx" | "other"; function getArtifactPreviewKind(filepath: string): ArtifactPreviewKind { @@ -636,9 +915,22 @@ function getArtifactPreviewKind(filepath: string): ArtifactPreviewKind { if (/\.(mp4|webm|ogg|mov|m4v)$/.test(lower)) return "video"; if (/\.(mp3|wav|ogg|m4a|aac|flac)$/.test(lower)) return "audio"; if (lower.endsWith(".pdf")) return "pdf"; + if (/\.(docx?)$/.test(lower)) return "docx"; + if (/\.(xlsx?)$/.test(lower)) return "xlsx"; + if (/\.(pptx?)$/.test(lower)) return "pptx"; return "other"; } +const OFFICE_PREVIEW_KINDS = new Set([ + "docx", + "xlsx", + "pptx", +]); + +function isOfficePreviewKind(kind: ArtifactPreviewKind) { + return OFFICE_PREVIEW_KINDS.has(kind); +} + function escapeHtml(value: string): string { return value .replaceAll("&", "&") diff --git a/frontend/src/components/workspace/messages/message-group.tsx b/frontend/src/components/workspace/messages/message-group.tsx index 53a4a101..05ab6302 100644 --- a/frontend/src/components/workspace/messages/message-group.tsx +++ b/frontend/src/components/workspace/messages/message-group.tsx @@ -39,6 +39,8 @@ import { Tooltip } from "../tooltip"; import { MarkdownContent } from "./markdown-content"; +const TOOL_CONTENT_COLLAPSE_THRESHOLD = 320; + export function MessageGroup({ className, messages, @@ -212,6 +214,33 @@ function ToolCall({ const { t } = useI18n(); const { setOpen, autoOpen, autoSelect, selectedArtifact, select } = useArtifacts(); + const [isCommandExpanded, setIsCommandExpanded] = useState(false); + + const ExpandableToolContent = ({ + content, + language = "bash", + expanded = false, + }: { + content: string; + language?: string; + expanded?: boolean; + }) => { + const shouldCollapse = content.length > TOOL_CONTENT_COLLAPSE_THRESHOLD; + const shouldShowCodeBlock = !shouldCollapse || expanded; + + return ( +
+ {shouldShowCodeBlock && ( + + )} +
+ ); + }; if (name === "web_search") { let label: React.ReactNode = t.toolCalls.searchForRelatedInfo; @@ -386,19 +415,31 @@ function ToolCall({ return t.toolCalls.executeCommand; } const command: string | undefined = (args as { command: string })?.command; + const shouldCollapse = !!command && command.length > TOOL_CONTENT_COLLAPSE_THRESHOLD; return ( { + event.stopPropagation(); + setIsCommandExpanded((prev) => !prev); + }} + > + {isCommandExpanded + ? t.toolCalls.collapseContent + : t.toolCalls.expandContent} + + ) + : undefined} > {command && ( - + )} ); diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts index 95b883da..77516abc 100644 --- a/frontend/src/core/i18n/locales/en-US.ts +++ b/frontend/src/core/i18n/locales/en-US.ts @@ -277,6 +277,8 @@ export const enUS: Translations = { writeFile: "Write file", clickToViewContent: "Click to view file content", writeTodos: "Update to-do list", + expandContent: "Expand", + collapseContent: "Collapse", skillInstallTooltip: "Install skill and make it available to DeerFlow", }, diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts index 251861be..b53df02d 100644 --- a/frontend/src/core/i18n/locales/types.ts +++ b/frontend/src/core/i18n/locales/types.ts @@ -208,6 +208,8 @@ export interface Translations { writeFile: string; clickToViewContent: string; writeTodos: string; + expandContent: string; + collapseContent: string; skillInstallTooltip: string; }; diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts index 6d5c159e..15e84469 100644 --- a/frontend/src/core/i18n/locales/zh-CN.ts +++ b/frontend/src/core/i18n/locales/zh-CN.ts @@ -265,6 +265,8 @@ export const zhCN: Translations = { writeFile: "写入文件", clickToViewContent: "点击查看文件内容", writeTodos: "更新 To-do 列表", + expandContent: "展开", + collapseContent: "收起", skillInstallTooltip: "安装技能并使其可在 DeerFlow 中使用", }, diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index 739e9c70..9bf4df9b 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -491,3 +491,11 @@ pre { overflow: hidden; contain: paint; } + +.pptx-preview-wrap { + height: 100%; +} + +.pptx-preview-wrap .pptx-preview-wrapper { + height: 100% !important; +} \ No newline at end of file