{t.artifactPreview.previewFailed}
@@ -119,8 +117,9 @@ export function ArtifactViewer({
) : (
diff --git a/README.md b/README.md index 429e70fd9..384856423 100644 --- a/README.md +++ b/README.md @@ -1311,6 +1311,8 @@ After each run, DeerFlow records a workspace change summary for the run-owned `w Files presented through `present_files` remain part of the thread's artifact state, and the Web UI restores the artifact panel and selected document after a page refresh. When a completed response successfully presents between 2 and 50 files, its final file card also offers one ZIP download. Archive membership comes from the terminal delivery receipt rather than browser-supplied paths, and the ZIP contains the current file versions, which may have changed since the response. The currently selected formal artifact is refreshed once when the run finishes so edits become visible without a manual reload. Existing UTF-8 text artifacts under `/mnt/user-data/outputs` can also be edited and explicitly saved from the panel on Unix and Windows while the thread is idle; saves use content revisions to prevent overwriting agent changes. +CSV and TSV artifacts open as tables in the artifact panel and in a separate window. The preview preserves text values (including leading zeros), supports an optional header row, and pages through up to 200 rows and 50 columns from the initial sample. Long or multiline cells can be opened and copied in full. Switch to source to inspect or edit the file; downloads and separate windows use the saved version. + Text artifacts are streamed with HTTP byte-range support. The Web UI initially loads at most 1 MiB, shows the preview size when a file is larger, and waits for an explicit **Load full file** action before fetching the remainder or mounting diff --git a/backend/tests/test_auth_me_permissions.py b/backend/tests/test_auth_me_permissions.py index 2622e3024..d51b2a19d 100644 --- a/backend/tests/test_auth_me_permissions.py +++ b/backend/tests/test_auth_me_permissions.py @@ -30,6 +30,9 @@ _ALL_PERMISSIONS = [ Permissions.RUNS_CREATE, Permissions.RUNS_READ, Permissions.RUNS_CANCEL, + Permissions.PROJECTS_READ, + Permissions.PROJECTS_WRITE, + Permissions.PROJECTS_DELETE, ] diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 7896e648c..721c0ac99 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -50,7 +50,7 @@ The frontend is a stateful chat application. Users create **threads** (conversat ### Source Layout (`src/`) -- **`app/`** — Next.js App Router. Routes include `/` (landing), `/showcase/[thread_id]` (allowlisted public read-only demos), `/workspace/chats/[thread_id]` (authenticated chat), `/workspace/agents/[agent_name]` and `/workspace/agents/new` (custom agents), `/artifacts/view` (chrome-free window that renders one markdown artifact with the panel's own renderer), `/blog/…`, the `(auth)/{login,setup,auth/callback}` flow, `/[lang]/docs/…`, and `/api/…` route handlers (e.g. `/api/memory`). +- **`app/`** — Next.js App Router. Routes include `/` (landing), `/showcase/[thread_id]` (allowlisted public read-only demos), `/workspace/chats/[thread_id]` (authenticated chat), `/workspace/agents/[agent_name]` and `/workspace/agents/new` (custom agents), `/artifacts/view` (chrome-free window that renders Markdown or CSV/TSV artifacts with the panel's own renderer), `/blog/…`, the `(auth)/{login,setup,auth/callback}` flow, `/[lang]/docs/…`, and `/api/…` route handlers (e.g. `/api/memory`). - **`components/`** — React components: - `ui/` — Shadcn UI primitives (auto-generated, ESLint-ignored) - `ai-elements/` — Vercel AI SDK elements (auto-generated, ESLint-ignored) @@ -125,3 +125,7 @@ Pin/archive responses must not merge unrelated metadata flags: out-of-order organization requests can otherwise roll back each other's confirmed state. Run-created optimistic snapshots have no archive flag: refresh archive-filtered lists from the server instead of inserting those snapshots into either view. + +### Delimited artifact preview + +CSV/TSV previews share `artifact-table-preview.tsx` between the panel and standalone viewer. Papa Parse runs only inside `delimited-preview.worker.ts`; `use-delimited-preview.ts` bounds input before transfer, cancels stale work, and enforces a five-second timeout. The parser detects the first record separator outside quoted fields and passes it explicitly to Papa Parse, so embedded newlines in an incomplete quoted field cannot corrupt newline detection. It retains at most 202 logical records and 50 columns, discarding an incomplete final record from truncated input. UI pagination displays at most 200 data rows in pages of 50. Keep the table mounted but inactive when switching to source so header/pagination state survives; changing file identity resets it. Pending `write_file` content stays in source mode until success. diff --git a/frontend/package.json b/frontend/package.json index 1ede350f4..c1525e829 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -81,6 +81,7 @@ "nextra-theme-docs": "^4.6.1", "nuxt-og-image": "^5.1.13", "ogl": "^1.0.11", + "papaparse": "5.7.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-resizable-panels": "^4.4.1", @@ -110,6 +111,7 @@ "@testing-library/react": "^16.3.2", "@types/gsap": "^3.0.0", "@types/node": "^20.14.10", + "@types/papaparse": "5.5.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "eslint": "^9.23.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 3cc5156ef..33be5d182 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -149,12 +149,12 @@ importers: embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.2.4) - gsap: - specifier: ^3.13.0 - version: 3.14.2 github-slugger: specifier: ^2.0.0 version: 2.0.0 + gsap: + specifier: ^3.13.0 + version: 3.14.2 h3: specifier: 1.15.9 version: 1.15.9 @@ -191,6 +191,9 @@ importers: ogl: specifier: ^1.0.11 version: 1.0.11 + papaparse: + specifier: 5.7.0 + version: 5.7.0 react: specifier: ^19.0.0 version: 19.2.4 @@ -203,12 +206,12 @@ importers: rehype-katex: specifier: ^7.0.1 version: 7.0.1 - rehype-sanitize: - specifier: ^6.0.0 - version: 6.0.0 rehype-raw: specifier: ^7.0.0 version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 rehype-slug: specifier: ^6.0.0 version: 6.0.0 @@ -273,6 +276,9 @@ importers: '@types/node': specifier: ^20.14.10 version: 20.19.33 + '@types/papaparse': + specifier: 5.5.2 + version: 5.5.2 '@types/react': specifier: ^19.0.0 version: 19.2.13 @@ -2480,6 +2486,9 @@ packages: '@types/node@20.19.33': resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} + '@types/papaparse@5.5.2': + resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4822,6 +4831,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + papaparse@5.7.0: + resolution: {integrity: sha512-qBGxg/7Q3Kl9Wfhrz2Z74UnvnHTXLNG6jmKJFeBvP2+y4lV7So+7SR62+Zd47JvdrCkX+nDcnr0ObPzek/+6RA==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -8149,6 +8161,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/papaparse@5.5.2': + dependencies: + '@types/node': 20.19.33 + '@types/react-dom@19.2.3(@types/react@19.2.13)': dependencies: '@types/react': 19.2.13 @@ -11221,6 +11237,8 @@ snapshots: pako@0.2.9: {} + papaparse@5.7.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 5717933da..0dd7a35a3 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -44,7 +44,10 @@ import { reconcileArtifactDraft, } from "@/core/artifacts/editing"; import { useArtifactContent } from "@/core/artifacts/hooks"; -import { getArtifactViewState } from "@/core/artifacts/preview"; +import { + getArtifactViewState, + getTabularDelimiter, +} from "@/core/artifacts/preview"; import { urlOfArtifact } from "@/core/artifacts/utils"; import { resolveArtifactOpenURL, @@ -169,9 +172,9 @@ export function ArtifactFileDetail({ const canPreviewInBrowser = useMemo(() => { return canBrowserPreviewFile(filepath); }, [filepath]); - const isSupportPreview = useMemo(() => { - return language === "html" || language === "markdown"; - }, [language]); + const isTabular = getTabularDelimiter(language) !== null; + const isSupportPreview = + language === "html" || language === "markdown" || isTabular; const toolResult = (() => { if (!isWriteFile) { return undefined; @@ -185,7 +188,9 @@ export function ArtifactFileDetail({ })(); const artifactViewState = getArtifactViewState({ filepath: filepathFromProps, - isSupportPreview, + isSupportPreview: + isSupportPreview && + (!isTabular || !isWriteFile || toolResult?.trim() === "OK"), toolResult, }); const { @@ -254,7 +259,7 @@ export function ArtifactFileDetail({ truncated && language === "html" ? "code" : viewMode; useEffect(() => { setViewMode(artifactViewState.initialViewMode); - }, [artifactViewState.initialViewMode]); + }, [artifactViewState.initialViewMode, filepathFromProps]); const confirmDiscard = useCallback(() => { return !isDirty || window.confirm(t.artifactEditing.discardChanges); @@ -420,7 +425,7 @@ export function ArtifactFileDetail({
{labels.failed}
+ ++ {limited ? labels.incomplete : labels.empty} +
+ ) : ( + <> ++ {result.columnCount > 50 && labels.columnsLimited}{" "} + {result.unevenRows && labels.uneven} +
+ )} +| + # + | + {columns.map((index) => ( ++ {hasHeader + ? (result.rows[0]?.[index] ?? "") + : labels.column(index + 1)} + | + ))} +
|---|---|
| + {start + rowIndex + 1} + | + {columns.map((column) => { + const value = row[column]; + const expandable = + value !== undefined && + (value.length > 120 || /[\r\n]/.test(value)); + return ( +
+
+ {value === undefined ? (
+
+ {labels.missing}
+
+ ) : expandable ? (
+
+ ) : (
+
+ {value}
+
+ )}
+
+ |
+ );
+ })}
+
{t.artifactPreview.previewFailed}
@@ -119,8 +117,9 @@ export function ArtifactViewer({
) : (