mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
## Thinking Path > - Paperclip helps people manage AI agents and inspect their work. > - Task file tabs show the files that agents produce. > - Markdown has a readable view, but CSV files show only source text. > - People need to scan CSV reports without a separate app. > - This pull request adds a table view to attachment and workspace file tabs. > - View buttons let people inspect the original source beside download. ## Linked Issues or Issue Description **Subsystem affected** ui/ — task file tabs. **Problem or motivation** CSV exports are difficult to scan as source text. Related work: https://github.com/paperclipai/paperclip/pull/14297 added text attachment tabs. **Proposed solution** Render CSV as a table by default. Add row numbers, sticky headers, and record counts. Keep rendered and raw icons beside download in task tabs. **Roadmap alignment** This is a small improvement to existing task file previews. It does not add a new core subsystem. ## What Changed - Add a shared CSV parser and table preview. - Handle quoted commas, escaped quotes, UTF-8 BOM, CRLF, and multiline cells. - Show at most 500 data rows and 100 columns. Show a notice for larger files. Keep the complete download. - Add CSV attachment and workspace previews. Put workspace tab view controls in the header. - Add parser and interaction tests, Storybook examples, and product documentation. ## Verification - 28 focused tests pass across the parser, attachment tabs, and file viewer. - UI typecheck, UI build, and token gates pass. - Playwright checked desktop and mobile rendered → raw → rendered flows using the real attachment preview component in Storybook. Screenshots are recorded on the driving task. - Repository-wide typecheck and build were attempted. Both require cargo, which is absent in this environment. - The earlier repository-wide local test run stopped after a server test race failure. The prior commit passed all GitHub CI jobs. - Review fixes add source-line navigation coverage and CSV boundary tests. All 22 file-viewer tests pass. UI typecheck and token gates pass. - All 54 active GitHub checks pass on the final commit. Two Storybook checks are skipped. Greptile gives 5/5 with no actionable findings. - The final version preserves HTML previews from master and uses the shared toolbar control. All 35 focused tests, UI typecheck, UI build, and token gates pass. ## Risks - The first CSV record supplies column headers. Files without headers use that same rule. - The table preview is bounded. Download and raw view retain the complete file. - No API, database, or dependency changes. ## Model Used - OpenAI Codex, GPT-6. The runtime does not expose a more precise model ID or context window. Used code editing, shell tools, and browser checks. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
42 lines
2.9 KiB
TypeScript
42 lines
2.9 KiB
TypeScript
import { useMemo } from "react";
|
|
import { parseCsv } from "@/lib/csv-preview";
|
|
|
|
const MAX_VISIBLE_ROWS = 500;
|
|
const MAX_VISIBLE_COLUMNS = 100;
|
|
|
|
export function CsvPreview({ text, title }: { text: string; title: string }) {
|
|
const parsed = useMemo(() => {
|
|
try { return { rows: parseCsv(text), error: null }; }
|
|
catch (error) { return { rows: [], error: (error as Error).message }; }
|
|
}, [text]);
|
|
if (parsed.error) return <p role="alert" className="p-4 text-sm text-muted-foreground">{parsed.error}</p>;
|
|
if (!parsed.rows.length) return <p className="p-4 text-sm text-muted-foreground">File is empty.</p>;
|
|
const [header, ...rows] = parsed.rows;
|
|
const columnCount = parsed.rows.reduce((count, row) => Math.max(count, row.length), 0);
|
|
const columns = Array.from({ length: Math.min(columnCount, MAX_VISIBLE_COLUMNS) }, (_, index) => index);
|
|
return (
|
|
<div className="flex min-h-0 flex-1 flex-col bg-background">
|
|
<div className="flex flex-wrap items-center gap-2 border-b border-border px-4 py-2 text-xs text-muted-foreground" role="status">
|
|
<span className="font-medium text-foreground">CSV preview</span>
|
|
<span>{rows.length.toLocaleString()} {rows.length === 1 ? "row" : "rows"}</span>
|
|
<span>{columnCount.toLocaleString()} {columnCount === 1 ? "column" : "columns"}</span>
|
|
{(rows.length > MAX_VISIBLE_ROWS || columnCount > MAX_VISIBLE_COLUMNS) && <span>Showing the first {Math.min(rows.length, MAX_VISIBLE_ROWS)} rows and {columns.length} columns. Download for the complete file.</span>}
|
|
</div>
|
|
<div className="min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset" role="region" aria-label={`${title} CSV table`} tabIndex={0}>
|
|
<table className="w-full border-collapse text-sm">
|
|
<caption className="sr-only">{title} — first record used as column headers</caption>
|
|
<thead className="sticky top-0 z-10 bg-muted">
|
|
<tr><th scope="col" className="w-10 border-b border-border px-3 py-3 text-right font-mono text-xs text-muted-foreground"><span className="sr-only">Row</span>#</th>
|
|
{columns.map((column) => <th key={column} scope="col" className="whitespace-nowrap border-b border-border px-4 py-3 text-left font-medium">{header[column] || `Column ${column + 1}`}</th>)}
|
|
</tr>
|
|
</thead>
|
|
<tbody>{rows.slice(0, MAX_VISIBLE_ROWS).map((row, index) => <tr key={index} className="even:bg-muted/30 hover:bg-accent/50">
|
|
<th scope="row" className="border-b border-border/50 px-3 py-2 text-right font-mono text-xs font-normal text-muted-foreground">{index + 1}</th>
|
|
{columns.map((column) => <td key={column} className="whitespace-pre-wrap border-b border-border/50 px-4 py-2 align-top">{row[column] ?? ""}</td>)}
|
|
</tr>)}</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|