@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
|
@@ -51,13 +51,16 @@ export interface FileArtifactPaneProps extends Omit<FilePreviewProps, "className
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* FileArtifactPane — opinionated artifact frame for file previews with tabs and
|
|
54
|
-
* header actions.
|
|
54
|
+
* header actions. The body is `FilePreview`, so every kind it renders (image,
|
|
55
|
+
* PDF, video, audio, CSV, spreadsheet, markdown, code, text) renders here too;
|
|
56
|
+
* `onDownload` drives both the header button and the body's download cards.
|
|
55
57
|
*/
|
|
56
58
|
export function FileArtifactPane({
|
|
57
59
|
filename,
|
|
58
60
|
content,
|
|
59
61
|
blobUrl,
|
|
60
62
|
mimeType,
|
|
63
|
+
size,
|
|
61
64
|
onClose,
|
|
62
65
|
onDownload,
|
|
63
66
|
path,
|
|
@@ -188,6 +191,8 @@ export function FileArtifactPane({
|
|
|
188
191
|
content={content}
|
|
189
192
|
blobUrl={blobUrl}
|
|
190
193
|
mimeType={mimeType}
|
|
194
|
+
size={size}
|
|
195
|
+
onDownload={onDownload}
|
|
191
196
|
hideHeader={true}
|
|
192
197
|
/>
|
|
193
198
|
</ArtifactPane>
|
package/src/files/file-format.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
export type FileFormat =
|
|
9
9
|
| "pdf"
|
|
10
10
|
| "image"
|
|
11
|
+
| "video"
|
|
12
|
+
| "audio"
|
|
11
13
|
| "csv"
|
|
12
14
|
| "spreadsheet"
|
|
13
15
|
| "code"
|
|
@@ -17,7 +19,20 @@ export type FileFormat =
|
|
|
17
19
|
| "text"
|
|
18
20
|
| "unknown";
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How a file surface renders a file. Every format maps to its own renderer;
|
|
24
|
+
* `unknown` becomes `binary`, which renders string content as text and offers a
|
|
25
|
+
* download for anything else.
|
|
26
|
+
*/
|
|
27
|
+
export type FilePreviewKind = Exclude<FileFormat, "unknown"> | "binary";
|
|
28
|
+
|
|
29
|
+
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp", "avif"];
|
|
30
|
+
const VIDEO_EXTENSIONS = ["mp4", "webm", "mov"];
|
|
31
|
+
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a"];
|
|
32
|
+
const SPREADSHEET_MIME_TYPES = [
|
|
33
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
34
|
+
"application/vnd.ms-excel",
|
|
35
|
+
];
|
|
21
36
|
|
|
22
37
|
const EXTENSION_TO_SYNTAX_LANGUAGE: Record<string, string> = {
|
|
23
38
|
ts: "typescript",
|
|
@@ -97,14 +112,19 @@ export function detectFileFormat(filename: string, mimeType?: string): FileForma
|
|
|
97
112
|
// 1. Specific MIME types are authoritative — they outrank the extension.
|
|
98
113
|
if (mime === "application/pdf") return "pdf";
|
|
99
114
|
if (mime.startsWith("image/")) return "image";
|
|
115
|
+
if (mime.startsWith("video/")) return "video";
|
|
116
|
+
if (mime.startsWith("audio/")) return "audio";
|
|
100
117
|
if (mime === "text/markdown") return "markdown";
|
|
101
118
|
if (mime === "application/json") return "json";
|
|
102
119
|
if (mime === "text/csv" || mime === "application/csv") return "csv";
|
|
120
|
+
if (SPREADSHEET_MIME_TYPES.includes(mime)) return "spreadsheet";
|
|
103
121
|
if (mime === "application/yaml" || mime === "application/x-yaml" || mime === "text/yaml") return "yaml";
|
|
104
122
|
|
|
105
123
|
// 2. Fall back to the file extension.
|
|
106
124
|
if (ext === "pdf") return "pdf";
|
|
107
125
|
if (IMAGE_EXTENSIONS.includes(ext)) return "image";
|
|
126
|
+
if (VIDEO_EXTENSIONS.includes(ext)) return "video";
|
|
127
|
+
if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
|
|
108
128
|
if (ext === "csv") return "csv";
|
|
109
129
|
if (ext === "xlsx" || ext === "xls") return "spreadsheet";
|
|
110
130
|
if (CODE_EXTENSIONS.has(ext)) return "code";
|
|
@@ -119,13 +139,28 @@ export function detectFileFormat(filename: string, mimeType?: string): FileForma
|
|
|
119
139
|
return "unknown";
|
|
120
140
|
}
|
|
121
141
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
142
|
+
/**
|
|
143
|
+
* The preview renderer for a file, from its name and optional MIME type only.
|
|
144
|
+
* Same precedence as `detectFileFormat` (MIME outranks extension); a file that
|
|
145
|
+
* matches nothing is `binary`. Content is not an input: the preview renders a
|
|
146
|
+
* `binary` file's string content as text and offers a download otherwise.
|
|
147
|
+
*/
|
|
148
|
+
export function resolveFilePreviewKind(filename: string, mimeType?: string): FilePreviewKind {
|
|
149
|
+
const format = detectFileFormat(filename, mimeType);
|
|
150
|
+
return format === "unknown" ? "binary" : format;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Human-facing label for a detected format or preview kind. */
|
|
154
|
+
export function getFormatLabel(format: FileFormat | FilePreviewKind): string {
|
|
124
155
|
switch (format) {
|
|
125
156
|
case "pdf":
|
|
126
157
|
return "PDF";
|
|
127
158
|
case "image":
|
|
128
159
|
return "Image";
|
|
160
|
+
case "video":
|
|
161
|
+
return "Video";
|
|
162
|
+
case "audio":
|
|
163
|
+
return "Audio";
|
|
129
164
|
case "csv":
|
|
130
165
|
return "CSV";
|
|
131
166
|
case "spreadsheet":
|
|
@@ -161,7 +196,10 @@ export function getSyntaxLanguage(filename: string): string | undefined {
|
|
|
161
196
|
* a MIME type on an extensionless file (where the extension can't reveal it);
|
|
162
197
|
* any other code format keys off the extension.
|
|
163
198
|
*/
|
|
164
|
-
export function getCodeLanguage(
|
|
199
|
+
export function getCodeLanguage(
|
|
200
|
+
filename: string,
|
|
201
|
+
format: FileFormat | FilePreviewKind,
|
|
202
|
+
): string | undefined {
|
|
165
203
|
if (format === "json" || format === "yaml") return format;
|
|
166
204
|
return getSyntaxLanguage(filename);
|
|
167
205
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
detectFileFormat,
|
|
4
|
+
getFormatLabel,
|
|
5
|
+
resolveFilePreviewKind,
|
|
6
|
+
type FilePreviewKind,
|
|
7
|
+
} from "./file-format";
|
|
8
|
+
|
|
9
|
+
describe("resolveFilePreviewKind", () => {
|
|
10
|
+
it.each<[string, string | undefined, FilePreviewKind]>([
|
|
11
|
+
// Images
|
|
12
|
+
["photo.png", undefined, "image"],
|
|
13
|
+
["photo.jpg", undefined, "image"],
|
|
14
|
+
["photo.jpeg", undefined, "image"],
|
|
15
|
+
["anim.gif", undefined, "image"],
|
|
16
|
+
["photo.webp", undefined, "image"],
|
|
17
|
+
["icon.svg", undefined, "image"],
|
|
18
|
+
["photo.avif", undefined, "image"],
|
|
19
|
+
// Documents
|
|
20
|
+
["report.pdf", undefined, "pdf"],
|
|
21
|
+
// Video
|
|
22
|
+
["clip.mp4", undefined, "video"],
|
|
23
|
+
["clip.webm", undefined, "video"],
|
|
24
|
+
["clip.mov", undefined, "video"],
|
|
25
|
+
// Audio
|
|
26
|
+
["track.mp3", undefined, "audio"],
|
|
27
|
+
["track.wav", undefined, "audio"],
|
|
28
|
+
["track.ogg", undefined, "audio"],
|
|
29
|
+
["track.m4a", undefined, "audio"],
|
|
30
|
+
// Tabular
|
|
31
|
+
["data.csv", undefined, "csv"],
|
|
32
|
+
["book.xlsx", undefined, "spreadsheet"],
|
|
33
|
+
["legacy.xls", undefined, "spreadsheet"],
|
|
34
|
+
// Prose and structured text
|
|
35
|
+
["README.md", undefined, "markdown"],
|
|
36
|
+
["notes.markdown", undefined, "markdown"],
|
|
37
|
+
["config.json", undefined, "json"],
|
|
38
|
+
["compose.yaml", undefined, "yaml"],
|
|
39
|
+
["compose.yml", undefined, "yaml"],
|
|
40
|
+
["server.ts", undefined, "code"],
|
|
41
|
+
["main.py", undefined, "code"],
|
|
42
|
+
["output.log", undefined, "text"],
|
|
43
|
+
["notes.txt", undefined, "text"],
|
|
44
|
+
])("%s (%s) → %s", (filename, mimeType, kind) => {
|
|
45
|
+
expect(resolveFilePreviewKind(filename, mimeType)).toBe(kind);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("lets an authoritative MIME type outrank the extension", () => {
|
|
49
|
+
expect(resolveFilePreviewKind("photo.txt", "image/png")).toBe("image");
|
|
50
|
+
expect(resolveFilePreviewKind("scan.png", "application/pdf")).toBe("pdf");
|
|
51
|
+
expect(resolveFilePreviewKind("clip.bin", "video/mp4")).toBe("video");
|
|
52
|
+
expect(resolveFilePreviewKind("track.webm", "audio/webm")).toBe("audio");
|
|
53
|
+
expect(
|
|
54
|
+
resolveFilePreviewKind(
|
|
55
|
+
"export.csv",
|
|
56
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
57
|
+
),
|
|
58
|
+
).toBe("spreadsheet");
|
|
59
|
+
expect(resolveFilePreviewKind("book.bin", "application/vnd.ms-excel")).toBe("spreadsheet");
|
|
60
|
+
expect(resolveFilePreviewKind("notes.json", "text/markdown")).toBe("markdown");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("resolves MIME-only files with no extension", () => {
|
|
64
|
+
expect(resolveFilePreviewKind("blob", "image/webp")).toBe("image");
|
|
65
|
+
expect(resolveFilePreviewKind("blob", "video/webm")).toBe("video");
|
|
66
|
+
expect(resolveFilePreviewKind("blob", "audio/mpeg")).toBe("audio");
|
|
67
|
+
expect(resolveFilePreviewKind("blob", "application/json; charset=utf-8")).toBe("json");
|
|
68
|
+
expect(resolveFilePreviewKind("blob", "text/plain")).toBe("text");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("ignores a generic MIME type and keeps the extension", () => {
|
|
72
|
+
expect(resolveFilePreviewKind("photo.png", "application/octet-stream")).toBe("image");
|
|
73
|
+
expect(resolveFilePreviewKind("main.py", "text/plain")).toBe("code");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("resolves everything unmatched to binary", () => {
|
|
77
|
+
expect(resolveFilePreviewKind("mystery.bin")).toBe("binary");
|
|
78
|
+
expect(resolveFilePreviewKind("noextension")).toBe("binary");
|
|
79
|
+
expect(resolveFilePreviewKind("weights", "application/octet-stream")).toBe("binary");
|
|
80
|
+
expect(resolveFilePreviewKind("pdf")).toBe("binary");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("agrees with detectFileFormat for every format that has a renderer", () => {
|
|
84
|
+
for (const filename of [
|
|
85
|
+
"photo.png",
|
|
86
|
+
"report.pdf",
|
|
87
|
+
"clip.mp4",
|
|
88
|
+
"track.mp3",
|
|
89
|
+
"data.csv",
|
|
90
|
+
"book.xlsx",
|
|
91
|
+
"README.md",
|
|
92
|
+
"config.json",
|
|
93
|
+
"compose.yml",
|
|
94
|
+
"server.ts",
|
|
95
|
+
"notes.txt",
|
|
96
|
+
]) {
|
|
97
|
+
expect(resolveFilePreviewKind(filename)).toBe(detectFileFormat(filename));
|
|
98
|
+
}
|
|
99
|
+
expect(detectFileFormat("mystery.bin")).toBe("unknown");
|
|
100
|
+
expect(resolveFilePreviewKind("mystery.bin")).toBe("binary");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("labels every preview kind", () => {
|
|
104
|
+
expect(getFormatLabel("video")).toBe("Video");
|
|
105
|
+
expect(getFormatLabel("audio")).toBe("Audio");
|
|
106
|
+
expect(getFormatLabel("image")).toBe("Image");
|
|
107
|
+
expect(getFormatLabel("binary")).toBe("File");
|
|
108
|
+
});
|
|
109
|
+
});
|