@tangle-network/ui 11.7.0 → 11.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/chat.js +8 -7
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/{chunk-UYTRNR7Z.js → chunk-565EATZZ.js} +1 -1
- package/dist/{chunk-4ENFLPWQ.js → chunk-BWOUSEHM.js} +1 -1
- package/dist/{chunk-XT4JMYVB.js → chunk-KNYSKNZX.js} +5 -3
- package/dist/{chunk-EIUE5GV2.js → chunk-ND3ZFXQK.js} +1 -44
- package/dist/{chunk-AAUNOHVL.js → chunk-NWECTSOH.js} +1 -1
- package/dist/{chunk-CVX53NCC.js → chunk-OXZRCXJK.js} +5 -5
- package/dist/{chunk-6RIX7M27.js → chunk-Q7ZR6LSD.js} +1 -1
- package/dist/{chunk-VDCZHOOO.js → chunk-X53RZSX6.js} +4 -4
- package/dist/{chunk-PLWRAS5Z.js → chunk-XGOWUDDW.js} +270 -106
- package/dist/{chunk-ZRVH3WCA.js → chunk-XU3HB5QL.js} +21 -1
- package/dist/{chunk-LFVF2HFB.js → chunk-YTO5V6QS.js} +1 -1
- package/dist/files.d.ts +41 -12
- package/dist/files.js +13 -5
- package/dist/hooks.js +8 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -22
- package/dist/run.js +8 -7
- package/dist/sdk-hooks.js +8 -7
- package/dist/tool-previews.js +2 -2
- package/dist/utils.js +6 -4
- package/package.json +1 -1
- package/src/files/file-artifact-pane.stories.tsx +149 -0
- package/src/files/file-artifact-pane.test.tsx +192 -0
- package/src/files/file-artifact-pane.tsx +6 -1
- package/src/files/file-format.ts +42 -4
- package/src/files/file-preview-kind.test.ts +109 -0
- package/src/files/file-preview.tsx +315 -132
- package/src/files/index.ts +8 -1
package/dist/files.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CSV_PREVIEW_ROW_LIMIT,
|
|
2
3
|
FileArtifactPane,
|
|
3
4
|
FilePreview,
|
|
4
5
|
FileTabs,
|
|
5
6
|
FileTree,
|
|
6
7
|
RichFileTree,
|
|
7
|
-
filterFileTree
|
|
8
|
-
|
|
8
|
+
filterFileTree,
|
|
9
|
+
parseCsv
|
|
10
|
+
} from "./chunk-XGOWUDDW.js";
|
|
11
|
+
import "./chunk-4CLN43XT.js";
|
|
9
12
|
import {
|
|
10
13
|
detectFileFormat,
|
|
11
14
|
fileExtension,
|
|
12
15
|
getCodeLanguage,
|
|
13
16
|
getFormatLabel,
|
|
14
|
-
getSyntaxLanguage
|
|
15
|
-
|
|
17
|
+
getSyntaxLanguage,
|
|
18
|
+
resolveFilePreviewKind
|
|
19
|
+
} from "./chunk-XU3HB5QL.js";
|
|
20
|
+
import "./chunk-MKTSMWVD.js";
|
|
16
21
|
import "./chunk-LTEKI7YJ.js";
|
|
17
22
|
import "./chunk-P5F43A65.js";
|
|
18
23
|
import "./chunk-WUQDUBJG.js";
|
|
19
24
|
import "./chunk-RQHJBTEU.js";
|
|
20
25
|
export {
|
|
26
|
+
CSV_PREVIEW_ROW_LIMIT,
|
|
21
27
|
FileArtifactPane,
|
|
22
28
|
FilePreview,
|
|
23
29
|
FileTabs,
|
|
@@ -28,5 +34,7 @@ export {
|
|
|
28
34
|
filterFileTree,
|
|
29
35
|
getCodeLanguage,
|
|
30
36
|
getFormatLabel,
|
|
31
|
-
getSyntaxLanguage
|
|
37
|
+
getSyntaxLanguage,
|
|
38
|
+
parseCsv,
|
|
39
|
+
resolveFilePreviewKind
|
|
32
40
|
};
|
package/dist/hooks.js
CHANGED
|
@@ -11,19 +11,20 @@ import {
|
|
|
11
11
|
useSSEStream,
|
|
12
12
|
useSdkSession,
|
|
13
13
|
useToolCallStream
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-BWOUSEHM.js";
|
|
15
15
|
import "./chunk-OEX7NZE3.js";
|
|
16
16
|
import {
|
|
17
17
|
useAutoScroll,
|
|
18
18
|
useRunCollapseState,
|
|
19
19
|
useRunGroups
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import "./chunk-
|
|
22
|
-
import "./chunk-
|
|
23
|
-
import "./chunk-
|
|
24
|
-
import "./chunk-
|
|
20
|
+
} from "./chunk-565EATZZ.js";
|
|
21
|
+
import "./chunk-Q7ZR6LSD.js";
|
|
22
|
+
import "./chunk-KNYSKNZX.js";
|
|
23
|
+
import "./chunk-ND3ZFXQK.js";
|
|
24
|
+
import "./chunk-NWECTSOH.js";
|
|
25
25
|
import "./chunk-L57WRIND.js";
|
|
26
|
-
import "./chunk-
|
|
26
|
+
import "./chunk-4CLN43XT.js";
|
|
27
|
+
import "./chunk-XU3HB5QL.js";
|
|
27
28
|
import "./chunk-P5F43A65.js";
|
|
28
29
|
import "./chunk-WUQDUBJG.js";
|
|
29
30
|
import "./chunk-RQHJBTEU.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, Agen
|
|
|
6
6
|
export { ExpandedToolDetail, ExpandedToolDetailProps, InlineThinkingItem, InlineThinkingItemProps, InlineToolItem, InlineToolItemProps, LiveDuration, RunGroup, RunGroupProps, RunRowShell, RunRowShellProps, RunRowStatus, RunRowStatusDot } from './run.js';
|
|
7
7
|
export { F as FeedSegment, T as ToolCallData, a as ToolCallFeed, b as ToolCallFeedProps, c as ToolCallStatus, d as ToolCallType, p as parseToolEvent } from './tool-call-feed-D9iofJgW.js';
|
|
8
8
|
export { OpenUIAction, OpenUIActionsNode, OpenUIArtifactRenderer, OpenUIArtifactRendererProps, OpenUIBadgeNode, OpenUICardNode, OpenUICodeNode, OpenUIComponentNode, OpenUIGridNode, OpenUIHeadingNode, OpenUIKeyValueNode, OpenUIMarkdownNode, OpenUIPrimitive, OpenUISeparatorNode, OpenUIStackNode, OpenUIStatNode, OpenUITableNode, OpenUITextNode } from './openui.js';
|
|
9
|
-
export { FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage } from './files.js';
|
|
9
|
+
export { CSV_PREVIEW_ROW_LIMIT, FileArtifactPane, FileArtifactPaneProps, FileFormat, FileNode, FilePreview, FilePreviewKind, FilePreviewProps, FileTabData, FileTabs, FileTabsProps, FileTree, FileTreeProps, FileTreeVisibilityOptions, RichFileTree, RichFileTreeGitEntry, RichFileTreeGitStatus, RichFileTreeProps, RichFileTreeThemeVars, detectFileFormat, fileExtension, filterFileTree, getCodeLanguage, getFormatLabel, getSyntaxLanguage, parseCsv, resolveFilePreviewKind } from './files.js';
|
|
10
10
|
export { Markdown, MarkdownProps } from './markdown.js';
|
|
11
11
|
export { C as CodeBlock, a as CodeBlockProps, b as CopyButton } from './code-block-0kSpWMnf.js';
|
|
12
12
|
export { AuthHeader, AuthHeaderProps, GitHubLoginButton, GitHubLoginButtonProps, LoginLayout, LoginLayoutProps, SessionUser, UserMenu, UserMenuProps } from './auth.js';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
useSSEStream,
|
|
21
21
|
useSdkSession,
|
|
22
22
|
useToolCallStream
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-BWOUSEHM.js";
|
|
24
24
|
import {
|
|
25
25
|
addMessage,
|
|
26
26
|
addParts,
|
|
@@ -146,39 +146,35 @@ import {
|
|
|
146
146
|
MessageList,
|
|
147
147
|
ThinkingIndicator,
|
|
148
148
|
UserMessage
|
|
149
|
-
} from "./chunk-
|
|
149
|
+
} from "./chunk-X53RZSX6.js";
|
|
150
150
|
import {
|
|
151
151
|
useAutoScroll,
|
|
152
152
|
useRunCollapseState,
|
|
153
153
|
useRunGroups
|
|
154
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-565EATZZ.js";
|
|
155
155
|
import {
|
|
156
156
|
LiveDuration
|
|
157
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-YTO5V6QS.js";
|
|
158
158
|
import {
|
|
159
159
|
ToolCallFeed,
|
|
160
160
|
parseToolEvent
|
|
161
|
-
} from "./chunk-
|
|
161
|
+
} from "./chunk-Q7ZR6LSD.js";
|
|
162
162
|
import {
|
|
163
163
|
InlineThinkingItem,
|
|
164
164
|
RunGroup
|
|
165
|
-
} from "./chunk-
|
|
165
|
+
} from "./chunk-OXZRCXJK.js";
|
|
166
166
|
import {
|
|
167
167
|
ExpandedToolDetail,
|
|
168
168
|
InlineToolItem,
|
|
169
169
|
RunRowShell,
|
|
170
170
|
RunRowStatusDot
|
|
171
|
-
} from "./chunk-
|
|
171
|
+
} from "./chunk-KNYSKNZX.js";
|
|
172
172
|
import {
|
|
173
173
|
TOOL_CATEGORY_ICONS,
|
|
174
|
-
formatBytes,
|
|
175
|
-
formatDuration,
|
|
176
|
-
formatUptime,
|
|
177
174
|
getToolCategory,
|
|
178
175
|
getToolDisplayMetadata,
|
|
179
|
-
getToolErrorText
|
|
180
|
-
|
|
181
|
-
} from "./chunk-EIUE5GV2.js";
|
|
176
|
+
getToolErrorText
|
|
177
|
+
} from "./chunk-ND3ZFXQK.js";
|
|
182
178
|
import {
|
|
183
179
|
CommandPreview,
|
|
184
180
|
DiffPreview,
|
|
@@ -187,7 +183,7 @@ import {
|
|
|
187
183
|
QuestionPreview,
|
|
188
184
|
WebSearchPreview,
|
|
189
185
|
WriteFilePreview
|
|
190
|
-
} from "./chunk-
|
|
186
|
+
} from "./chunk-NWECTSOH.js";
|
|
191
187
|
import {
|
|
192
188
|
TextShimmer
|
|
193
189
|
} from "./chunk-L57WRIND.js";
|
|
@@ -215,24 +211,33 @@ import {
|
|
|
215
211
|
badgeVariants
|
|
216
212
|
} from "./chunk-GYPQXTJU.js";
|
|
217
213
|
import {
|
|
218
|
-
|
|
219
|
-
buttonVariants
|
|
220
|
-
} from "./chunk-MKTSMWVD.js";
|
|
221
|
-
import {
|
|
214
|
+
CSV_PREVIEW_ROW_LIMIT,
|
|
222
215
|
FileArtifactPane,
|
|
223
216
|
FilePreview,
|
|
224
217
|
FileTabs,
|
|
225
218
|
FileTree,
|
|
226
219
|
RichFileTree,
|
|
227
|
-
filterFileTree
|
|
228
|
-
|
|
220
|
+
filterFileTree,
|
|
221
|
+
parseCsv
|
|
222
|
+
} from "./chunk-XGOWUDDW.js";
|
|
223
|
+
import {
|
|
224
|
+
formatBytes,
|
|
225
|
+
formatDuration,
|
|
226
|
+
formatUptime,
|
|
227
|
+
truncateText
|
|
228
|
+
} from "./chunk-4CLN43XT.js";
|
|
229
229
|
import {
|
|
230
230
|
detectFileFormat,
|
|
231
231
|
fileExtension,
|
|
232
232
|
getCodeLanguage,
|
|
233
233
|
getFormatLabel,
|
|
234
|
-
getSyntaxLanguage
|
|
235
|
-
|
|
234
|
+
getSyntaxLanguage,
|
|
235
|
+
resolveFilePreviewKind
|
|
236
|
+
} from "./chunk-XU3HB5QL.js";
|
|
237
|
+
import {
|
|
238
|
+
Button,
|
|
239
|
+
buttonVariants
|
|
240
|
+
} from "./chunk-MKTSMWVD.js";
|
|
236
241
|
import {
|
|
237
242
|
Tabs,
|
|
238
243
|
TabsContent,
|
|
@@ -262,6 +267,7 @@ export {
|
|
|
262
267
|
AvatarImage,
|
|
263
268
|
Badge,
|
|
264
269
|
Button,
|
|
270
|
+
CSV_PREVIEW_ROW_LIMIT,
|
|
265
271
|
Card,
|
|
266
272
|
CardContent,
|
|
267
273
|
CardDescription,
|
|
@@ -413,10 +419,12 @@ export {
|
|
|
413
419
|
hasBackgroundRunningSessions,
|
|
414
420
|
isStreamingAtom,
|
|
415
421
|
messagesAtom,
|
|
422
|
+
parseCsv,
|
|
416
423
|
parseToolEvent,
|
|
417
424
|
partMapAtom,
|
|
418
425
|
registerActiveSession,
|
|
419
426
|
resetActiveSessions,
|
|
427
|
+
resolveFilePreviewKind,
|
|
420
428
|
setActiveSessionAttention,
|
|
421
429
|
setActiveSessionConnection,
|
|
422
430
|
setActiveSessionError,
|
package/dist/run.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LiveDuration
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YTO5V6QS.js";
|
|
4
4
|
import {
|
|
5
5
|
ToolCallFeed,
|
|
6
6
|
parseToolEvent
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-Q7ZR6LSD.js";
|
|
8
8
|
import {
|
|
9
9
|
InlineThinkingItem,
|
|
10
10
|
RunGroup
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-OXZRCXJK.js";
|
|
12
12
|
import {
|
|
13
13
|
ExpandedToolDetail,
|
|
14
14
|
InlineToolItem,
|
|
15
15
|
RunRowShell,
|
|
16
16
|
RunRowStatusDot
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import "./chunk-
|
|
19
|
-
import "./chunk-
|
|
17
|
+
} from "./chunk-KNYSKNZX.js";
|
|
18
|
+
import "./chunk-ND3ZFXQK.js";
|
|
19
|
+
import "./chunk-NWECTSOH.js";
|
|
20
20
|
import "./chunk-L57WRIND.js";
|
|
21
21
|
import "./chunk-3M7BPSSW.js";
|
|
22
22
|
import "./chunk-GYPQXTJU.js";
|
|
23
|
+
import "./chunk-4CLN43XT.js";
|
|
24
|
+
import "./chunk-XU3HB5QL.js";
|
|
23
25
|
import "./chunk-MKTSMWVD.js";
|
|
24
|
-
import "./chunk-ZRVH3WCA.js";
|
|
25
26
|
import "./chunk-P5F43A65.js";
|
|
26
27
|
import "./chunk-WUQDUBJG.js";
|
|
27
28
|
import "./chunk-RQHJBTEU.js";
|
package/dist/sdk-hooks.js
CHANGED
|
@@ -5,19 +5,20 @@ import {
|
|
|
5
5
|
useSSEStream,
|
|
6
6
|
useSdkSession,
|
|
7
7
|
useToolCallStream
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-BWOUSEHM.js";
|
|
9
9
|
import "./chunk-OEX7NZE3.js";
|
|
10
10
|
import {
|
|
11
11
|
useAutoScroll,
|
|
12
12
|
useRunCollapseState,
|
|
13
13
|
useRunGroups
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-565EATZZ.js";
|
|
15
|
+
import "./chunk-Q7ZR6LSD.js";
|
|
16
|
+
import "./chunk-KNYSKNZX.js";
|
|
17
|
+
import "./chunk-ND3ZFXQK.js";
|
|
18
|
+
import "./chunk-NWECTSOH.js";
|
|
19
19
|
import "./chunk-L57WRIND.js";
|
|
20
|
-
import "./chunk-
|
|
20
|
+
import "./chunk-4CLN43XT.js";
|
|
21
|
+
import "./chunk-XU3HB5QL.js";
|
|
21
22
|
import "./chunk-P5F43A65.js";
|
|
22
23
|
import "./chunk-WUQDUBJG.js";
|
|
23
24
|
import "./chunk-RQHJBTEU.js";
|
package/dist/tool-previews.js
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
QuestionPreview,
|
|
8
8
|
WebSearchPreview,
|
|
9
9
|
WriteFilePreview
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import "./chunk-
|
|
10
|
+
} from "./chunk-NWECTSOH.js";
|
|
11
|
+
import "./chunk-XU3HB5QL.js";
|
|
12
12
|
import "./chunk-WUQDUBJG.js";
|
|
13
13
|
import "./chunk-RQHJBTEU.js";
|
|
14
14
|
export {
|
package/dist/utils.js
CHANGED
|
@@ -4,14 +4,16 @@ import {
|
|
|
4
4
|
} from "./chunk-XGKULLYE.js";
|
|
5
5
|
import {
|
|
6
6
|
TOOL_CATEGORY_ICONS,
|
|
7
|
+
getToolCategory,
|
|
8
|
+
getToolDisplayMetadata,
|
|
9
|
+
getToolErrorText
|
|
10
|
+
} from "./chunk-ND3ZFXQK.js";
|
|
11
|
+
import {
|
|
7
12
|
formatBytes,
|
|
8
13
|
formatDuration,
|
|
9
14
|
formatUptime,
|
|
10
|
-
getToolCategory,
|
|
11
|
-
getToolDisplayMetadata,
|
|
12
|
-
getToolErrorText,
|
|
13
15
|
truncateText
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-4CLN43XT.js";
|
|
15
17
|
import {
|
|
16
18
|
cn
|
|
17
19
|
} from "./chunk-RQHJBTEU.js";
|
package/package.json
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { FileArtifactPane } from "./file-artifact-pane";
|
|
3
|
+
|
|
4
|
+
const TS_SOURCE = `interface SandboxConfig {
|
|
5
|
+
model: string
|
|
6
|
+
timeout: number
|
|
7
|
+
env: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Create a new sandbox and stream its output.
|
|
11
|
+
export async function run(config: SandboxConfig) {
|
|
12
|
+
const sandbox = await Sandbox.create(config)
|
|
13
|
+
for await (const chunk of sandbox.stream()) {
|
|
14
|
+
process.stdout.write(chunk)
|
|
15
|
+
}
|
|
16
|
+
}`;
|
|
17
|
+
|
|
18
|
+
const MARKDOWN_SOURCE = `# Run report
|
|
19
|
+
|
|
20
|
+
The agent finished **3 of 3** tasks.
|
|
21
|
+
|
|
22
|
+
| Task | Result |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Build | pass |
|
|
25
|
+
| Test | 315 / 315 |
|
|
26
|
+
| Deploy | staging |
|
|
27
|
+
|
|
28
|
+
\`\`\`ts
|
|
29
|
+
export const ready = true
|
|
30
|
+
\`\`\`
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
// A transparent SVG, larger than the pane, so the checker ground shows through
|
|
34
|
+
// and the fit-to-pane / natural-size toggle has a visible effect.
|
|
35
|
+
const IMAGE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 640 360">
|
|
36
|
+
<defs>
|
|
37
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
38
|
+
<stop offset="0" stop-color="#8263FF"/>
|
|
39
|
+
<stop offset="1" stop-color="#10b981"/>
|
|
40
|
+
</linearGradient>
|
|
41
|
+
</defs>
|
|
42
|
+
<circle cx="220" cy="180" r="120" fill="url(#g)"/>
|
|
43
|
+
<rect x="360" y="80" width="200" height="200" rx="28" fill="#FFB347" opacity="0.9"/>
|
|
44
|
+
<text x="320" y="330" text-anchor="middle" font-family="sans-serif" font-size="22" fill="#E8E6F6">chart.svg · 1600 × 900</text>
|
|
45
|
+
</svg>`;
|
|
46
|
+
const IMAGE_URL = `data:image/svg+xml;utf8,${encodeURIComponent(IMAGE_SVG)}`;
|
|
47
|
+
|
|
48
|
+
// One-page PDF (Helvetica text and a rule) so the viewer has real content.
|
|
49
|
+
const PDF_URL =
|
|
50
|
+
"data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEgPj4KZW5kb2JqCjMgMCBvYmoKPDwgL1R5cGUgL1BhZ2UgL1BhcmVudCAyIDAgUiAvTWVkaWFCb3ggWzAgMCA2MTIgNzkyXSAvQ29udGVudHMgNCAwIFIgL1Jlc291cmNlcyA8PCAvRm9udCA8PCAvRjEgNSAwIFIgPj4gPj4gPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0xlbmd0aCAyODkgPj4Kc3RyZWFtCkJUIC9GMSAyMiBUZiA1NiA3MDAgVGQgKEZpbGVBcnRpZmFjdFBhbmUpIFRqIEVUCkJUIC9GMSAxMiBUZiA1NiA2NzIgVGQgKFBERiBwcmV2aWV3IHJlbmRlcnMgdGhlIGJsb2JVcmwgYXQgZnVsbCBwYW5lIGhlaWdodC4pIFRqIEVUCkJUIC9GMSAxMiBUZiA1NiA2NTIgVGQgKEV2ZXJ5IGNvbnN1bWVyIG9mIEB0YW5nbGUtbmV0d29yay91aS9maWxlcyBnZXRzIHRoaXMgZm9yIGZyZWUuKSBUaiBFVAo1NiA2MjAgbSA1NTYgNjIwIGwgUwpCVCAvRjEgMTAgVGYgNTYgNTk2IFRkIChQYWdlIDEgb2YgMSkgVGogRVQKZW5kc3RyZWFtCmVuZG9iago1IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBlIC9UeXBlMSAvQmFzZUZvbnQgL0hlbHZldGljYSA+PgplbmRvYmoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAwNTggMDAwMDAgbiAKMDAwMDAwMDExNSAwMDAwMCBuIAowMDAwMDAwMjQxIDAwMDAwIG4gCjAwMDAwMDA1ODEgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDEgMCBSID4+CnN0YXJ0eHJlZgo2NTEKJSVFT0YK";
|
|
51
|
+
|
|
52
|
+
const CSV_SOURCE = [
|
|
53
|
+
"id,name,role,note,active",
|
|
54
|
+
...Array.from({ length: 520 }, (_, i) =>
|
|
55
|
+
[
|
|
56
|
+
i + 1,
|
|
57
|
+
`"Lovelace, Ada ${i + 1}"`,
|
|
58
|
+
["engineer", "maintainer", "architect"][i % 3],
|
|
59
|
+
`"Handles ""quoted"" values, and commas"`,
|
|
60
|
+
i % 4 === 0 ? "false" : "true",
|
|
61
|
+
].join(","),
|
|
62
|
+
),
|
|
63
|
+
].join("\n");
|
|
64
|
+
|
|
65
|
+
// CC0 sample media hosted by MDN; the story only needs a playable src.
|
|
66
|
+
const VIDEO_URL = "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4";
|
|
67
|
+
const AUDIO_URL = "https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3";
|
|
68
|
+
|
|
69
|
+
const meta: Meta<typeof FileArtifactPane> = {
|
|
70
|
+
title: "Files/FileArtifactPane",
|
|
71
|
+
component: FileArtifactPane,
|
|
72
|
+
parameters: { layout: "fullscreen" },
|
|
73
|
+
argTypes: {
|
|
74
|
+
onDownload: { action: "download" },
|
|
75
|
+
onClose: { action: "close" },
|
|
76
|
+
},
|
|
77
|
+
decorators: [
|
|
78
|
+
(Story) => (
|
|
79
|
+
<div className="h-[560px] w-[760px] overflow-hidden rounded-xl border border-border bg-card text-foreground">
|
|
80
|
+
<Story />
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default meta;
|
|
87
|
+
type Story = StoryObj<typeof FileArtifactPane>;
|
|
88
|
+
|
|
89
|
+
export const Code: Story = {
|
|
90
|
+
args: { filename: "run.ts", path: "src/run.ts", content: TS_SOURCE },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Image: Story = {
|
|
94
|
+
args: {
|
|
95
|
+
filename: "chart.svg",
|
|
96
|
+
path: "out/chart.svg",
|
|
97
|
+
blobUrl: IMAGE_URL,
|
|
98
|
+
mimeType: "image/svg+xml",
|
|
99
|
+
size: 1_204,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const Pdf: Story = {
|
|
104
|
+
name: "PDF",
|
|
105
|
+
args: {
|
|
106
|
+
filename: "report.pdf",
|
|
107
|
+
path: "out/report.pdf",
|
|
108
|
+
blobUrl: PDF_URL,
|
|
109
|
+
mimeType: "application/pdf",
|
|
110
|
+
size: 834,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const Csv: Story = {
|
|
115
|
+
name: "CSV (520 rows, capped at 500)",
|
|
116
|
+
args: { filename: "people.csv", path: "data/people.csv", content: CSV_SOURCE },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Video: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
filename: "flower.mp4",
|
|
122
|
+
path: "media/flower.mp4",
|
|
123
|
+
blobUrl: VIDEO_URL,
|
|
124
|
+
mimeType: "video/mp4",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const Audio: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
filename: "t-rex-roar.mp3",
|
|
131
|
+
path: "media/t-rex-roar.mp3",
|
|
132
|
+
blobUrl: AUDIO_URL,
|
|
133
|
+
mimeType: "audio/mpeg",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const Spreadsheet: Story = {
|
|
138
|
+
args: { filename: "ledger.xlsx", path: "finance/ledger.xlsx", size: 48_213 },
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const MarkdownProse: Story = {
|
|
142
|
+
name: "Markdown",
|
|
143
|
+
args: { filename: "REPORT.md", path: "out/REPORT.md", content: MARKDOWN_SOURCE },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const BinaryWithoutLink: Story = {
|
|
147
|
+
name: "Binary without a download link",
|
|
148
|
+
args: { filename: "weights.bin", path: "model/weights.bin", size: 402_653_184 },
|
|
149
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import { FileArtifactPane } from "./file-artifact-pane";
|
|
4
|
+
import { CSV_PREVIEW_ROW_LIMIT, parseCsv } from "./file-preview";
|
|
5
|
+
|
|
6
|
+
const BLOB_URL = "blob:https://app.tangle.tools/1f2e3d4c";
|
|
7
|
+
|
|
8
|
+
function csvWithRows(rowCount: number): string {
|
|
9
|
+
const rows = Array.from({ length: rowCount }, (_, i) => `"Ada, Countess ${i}",x,${i}`);
|
|
10
|
+
return ["name,note,n", ...rows].join("\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("FileArtifactPane previews", () => {
|
|
14
|
+
it("renders an image from blobUrl and toggles natural size on click", () => {
|
|
15
|
+
const { container } = render(
|
|
16
|
+
<FileArtifactPane filename="chart.png" blobUrl={BLOB_URL} mimeType="image/png" />,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const img = container.querySelector("img");
|
|
20
|
+
expect(img).not.toBeNull();
|
|
21
|
+
expect(img).toHaveAttribute("src", BLOB_URL);
|
|
22
|
+
expect(img).toHaveAttribute("alt", "chart.png");
|
|
23
|
+
expect(img).toHaveClass("object-contain");
|
|
24
|
+
|
|
25
|
+
const toggle = screen.getByRole("button", { name: "Show image at natural size" });
|
|
26
|
+
expect(toggle).toHaveAttribute("aria-pressed", "false");
|
|
27
|
+
fireEvent.click(toggle);
|
|
28
|
+
expect(screen.getByRole("button", { name: "Fit image to pane" })).toHaveAttribute(
|
|
29
|
+
"aria-pressed",
|
|
30
|
+
"true",
|
|
31
|
+
);
|
|
32
|
+
expect(container.querySelector("img")).toHaveClass("max-w-none");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renders a PDF as an <object> of the blobUrl", () => {
|
|
36
|
+
const { container } = render(
|
|
37
|
+
<FileArtifactPane filename="report.pdf" blobUrl={BLOB_URL} mimeType="application/pdf" />,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const viewer = container.querySelector("object");
|
|
41
|
+
expect(viewer).not.toBeNull();
|
|
42
|
+
expect(viewer).toHaveAttribute("data", BLOB_URL);
|
|
43
|
+
expect(viewer).toHaveAttribute("type", "application/pdf");
|
|
44
|
+
expect(viewer).toHaveAttribute("title", "report.pdf");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders video and audio with native controls", () => {
|
|
48
|
+
const video = render(<FileArtifactPane filename="demo.mp4" blobUrl={BLOB_URL} />);
|
|
49
|
+
const videoElement = video.container.querySelector("video");
|
|
50
|
+
expect(videoElement).not.toBeNull();
|
|
51
|
+
expect(videoElement).toHaveAttribute("src", BLOB_URL);
|
|
52
|
+
expect(videoElement).toHaveAttribute("controls");
|
|
53
|
+
video.unmount();
|
|
54
|
+
|
|
55
|
+
const audio = render(<FileArtifactPane filename="voice.m4a" blobUrl={BLOB_URL} />);
|
|
56
|
+
const audioElement = audio.container.querySelector("audio");
|
|
57
|
+
expect(audioElement).not.toBeNull();
|
|
58
|
+
expect(audioElement).toHaveAttribute("src", BLOB_URL);
|
|
59
|
+
expect(audioElement).toHaveAttribute("controls");
|
|
60
|
+
expect(audio.getAllByText("voice.m4a").length).toBeGreaterThan(1); // header + player label
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("parses quoted commas in CSV and caps the table at the row limit", () => {
|
|
64
|
+
const total = CSV_PREVIEW_ROW_LIMIT + 1;
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<FileArtifactPane filename="people.csv" content={csvWithRows(total)} />,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const headers = Array.from(container.querySelectorAll("thead th")).map((th) => th.textContent);
|
|
70
|
+
expect(headers).toEqual(["name", "note", "n"]);
|
|
71
|
+
expect(container.querySelectorAll("thead th")[0]).toHaveClass("sticky");
|
|
72
|
+
|
|
73
|
+
const bodyRows = container.querySelectorAll("tbody tr");
|
|
74
|
+
expect(bodyRows).toHaveLength(CSV_PREVIEW_ROW_LIMIT);
|
|
75
|
+
expect(bodyRows[0].querySelectorAll("td")[0]).toHaveTextContent("Ada, Countess 0");
|
|
76
|
+
expect(bodyRows[0].querySelectorAll("td")).toHaveLength(3);
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
screen.getByText(`Showing ${CSV_PREVIEW_ROW_LIMIT} of ${total} rows · 3 columns`),
|
|
80
|
+
).toBeInTheDocument();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reports the row count when the CSV is under the cap", () => {
|
|
84
|
+
render(<FileArtifactPane filename="people.csv" content={csvWithRows(3)} />);
|
|
85
|
+
expect(screen.getByText("3 rows · 3 columns")).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("renders a spreadsheet as a download card and calls onDownload", () => {
|
|
89
|
+
const onDownload = vi.fn();
|
|
90
|
+
render(
|
|
91
|
+
<FileArtifactPane filename="book.xlsx" size={1536} onDownload={onDownload} />,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(screen.getByText("1.5 KB")).toBeInTheDocument();
|
|
95
|
+
expect(
|
|
96
|
+
screen.getByText("Download to open this workbook in a spreadsheet app."),
|
|
97
|
+
).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
fireEvent.click(screen.getByRole("button", { name: "Download" }));
|
|
100
|
+
expect(onDownload).toHaveBeenCalledTimes(1);
|
|
101
|
+
|
|
102
|
+
// The header keeps its own download control.
|
|
103
|
+
fireEvent.click(screen.getByRole("button", { name: "Download book.xlsx" }));
|
|
104
|
+
expect(onDownload).toHaveBeenCalledTimes(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("renders markdown through the package Markdown renderer", () => {
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<FileArtifactPane filename="README.md" content={"# Title\n\nBody with **bold**."} />,
|
|
110
|
+
);
|
|
111
|
+
expect(container.querySelector(".tangle-prose")).not.toBeNull();
|
|
112
|
+
expect(container.querySelector("h1")).toHaveTextContent("Title");
|
|
113
|
+
expect(container.querySelector("strong")).toHaveTextContent("bold");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("renders a binary file with no download link as one muted sentence", () => {
|
|
117
|
+
const onDownload = vi.fn();
|
|
118
|
+
const withDownload = render(
|
|
119
|
+
<FileArtifactPane filename="weights.bin" onDownload={onDownload} />,
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
withDownload.getByText("This file needs a download link to preview."),
|
|
123
|
+
).toBeInTheDocument();
|
|
124
|
+
fireEvent.click(withDownload.getByRole("button", { name: "Download" }));
|
|
125
|
+
expect(onDownload).toHaveBeenCalledTimes(1);
|
|
126
|
+
withDownload.unmount();
|
|
127
|
+
|
|
128
|
+
render(<FileArtifactPane filename="weights.bin" />);
|
|
129
|
+
expect(screen.getByText("This file needs a download link to preview.")).toBeInTheDocument();
|
|
130
|
+
expect(screen.queryByRole("button", { name: "Download" })).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("renders the same sentence for an image, PDF, or video without a blobUrl", () => {
|
|
134
|
+
for (const filename of ["chart.png", "report.pdf", "demo.mp4"]) {
|
|
135
|
+
const { getByText, unmount } = render(<FileArtifactPane filename={filename} />);
|
|
136
|
+
expect(getByText("This file needs a download link to preview.")).toBeInTheDocument();
|
|
137
|
+
unmount();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("renders an unknown file's text content as text", () => {
|
|
142
|
+
const { container } = render(
|
|
143
|
+
<FileArtifactPane filename="Makefile.custom" content={"build:\n\tgo build"} />,
|
|
144
|
+
);
|
|
145
|
+
expect(container.querySelector("pre")).toHaveTextContent("go build");
|
|
146
|
+
expect(container.querySelector("code")).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps code on the syntax highlighter and text on a plain pre", () => {
|
|
150
|
+
const code = render(
|
|
151
|
+
<FileArtifactPane filename="server.ts" content={"const x = 1;\nexport { x };"} />,
|
|
152
|
+
);
|
|
153
|
+
expect(code.container.querySelector("code")).not.toBeNull();
|
|
154
|
+
expect(code.container.querySelector("table")).toBeNull();
|
|
155
|
+
expect(code.getByText(/ts · 2 lines/)).toBeInTheDocument();
|
|
156
|
+
code.unmount();
|
|
157
|
+
|
|
158
|
+
const text = render(<FileArtifactPane filename="notes.txt" content="just words" />);
|
|
159
|
+
expect(text.container.querySelector("pre")).toHaveTextContent("just words");
|
|
160
|
+
expect(text.container.querySelector("code")).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("renders SVG source as code when the host read it as text", () => {
|
|
164
|
+
const { container } = render(
|
|
165
|
+
<FileArtifactPane filename="icon.svg" content={'<svg xmlns="http://www.w3.org/2000/svg"/>'} />,
|
|
166
|
+
);
|
|
167
|
+
expect(container.querySelector("code")).not.toBeNull();
|
|
168
|
+
expect(container.querySelector("img")).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("parseCsv", () => {
|
|
173
|
+
it("keeps commas, line breaks, and doubled quotes inside quoted cells", () => {
|
|
174
|
+
expect(parseCsv('a,b\n"x, y","line 1\nline 2"\n"say ""hi""",z')).toEqual([
|
|
175
|
+
["a", "b"],
|
|
176
|
+
["x, y", "line 1\nline 2"],
|
|
177
|
+
['say "hi"', "z"],
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("trims unquoted cells and accepts CRLF, blank lines, and a trailing line break", () => {
|
|
182
|
+
expect(parseCsv("a, b\r\n\r\n1 ,2\r\n")).toEqual([
|
|
183
|
+
["a", "b"],
|
|
184
|
+
["1", "2"],
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("returns no rows for empty input", () => {
|
|
189
|
+
expect(parseCsv("")).toEqual([]);
|
|
190
|
+
expect(parseCsv("\n\n")).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
});
|