@tangle-network/ui 1.0.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 +12 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/active-sessions-store-CeOmXgv5.d.ts +85 -0
- package/dist/artifact-pane-DvJyPWV4.d.ts +24 -0
- package/dist/auth.d.ts +74 -0
- package/dist/auth.js +15 -0
- package/dist/button-CMQuQEW_.d.ts +17 -0
- package/dist/chat.d.ts +232 -0
- package/dist/chat.js +30 -0
- package/dist/chunk-2NFQRQOD.js +1009 -0
- package/dist/chunk-2VH6PUXD.js +186 -0
- package/dist/chunk-34A66VBG.js +214 -0
- package/dist/chunk-3OI2QKFD.js +0 -0
- package/dist/chunk-4CLN43XT.js +45 -0
- package/dist/chunk-54SQQMMM.js +156 -0
- package/dist/chunk-5Z5ZYMOJ.js +0 -0
- package/dist/chunk-66BNMOVT.js +167 -0
- package/dist/chunk-6BGQA4BQ.js +0 -0
- package/dist/chunk-7UO2ZMRQ.js +133 -0
- package/dist/chunk-BX6AQMUS.js +183 -0
- package/dist/chunk-CD53GZOM.js +59 -0
- package/dist/chunk-CSAIKY36.js +54 -0
- package/dist/chunk-EEE55AVS.js +1201 -0
- package/dist/chunk-GYPQXTJU.js +230 -0
- package/dist/chunk-HFL6R6IF.js +37 -0
- package/dist/chunk-HJKCSXCH.js +737 -0
- package/dist/chunk-LISXUB4D.js +1222 -0
- package/dist/chunk-LQS34IGP.js +0 -0
- package/dist/chunk-MKTSMWVD.js +109 -0
- package/dist/chunk-NKDZ7GZE.js +192 -0
- package/dist/chunk-OEX7NZE3.js +321 -0
- package/dist/chunk-Q56BYXQF.js +61 -0
- package/dist/chunk-Q7EIIWTC.js +0 -0
- package/dist/chunk-REJESC5U.js +117 -0
- package/dist/chunk-RQGKSCEZ.js +0 -0
- package/dist/chunk-RQHJBTEU.js +10 -0
- package/dist/chunk-TMFOPHHN.js +299 -0
- package/dist/chunk-XGKULLYE.js +40 -0
- package/dist/chunk-XIHMJ7ZQ.js +614 -0
- package/dist/chunk-YJ2G3XO5.js +1048 -0
- package/dist/chunk-YNN4O57I.js +754 -0
- package/dist/code-block-DjXf8eOG.d.ts +19 -0
- package/dist/document-editor-pane-A5LT5H4N.js +12 -0
- package/dist/document-editor-pane-DyDEX_Zm.d.ts +124 -0
- package/dist/editor.d.ts +120 -0
- package/dist/editor.js +34 -0
- package/dist/files.d.ts +175 -0
- package/dist/files.js +20 -0
- package/dist/hooks.d.ts +56 -0
- package/dist/hooks.js +41 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +446 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +14 -0
- package/dist/message-BHWbxBtT.d.ts +15 -0
- package/dist/openui.d.ts +115 -0
- package/dist/openui.js +12 -0
- package/dist/parts-dj7AcUg0.d.ts +36 -0
- package/dist/primitives.d.ts +332 -0
- package/dist/primitives.js +191 -0
- package/dist/run-PfLmDAox.d.ts +41 -0
- package/dist/run.d.ts +69 -0
- package/dist/run.js +36 -0
- package/dist/sdk-hooks.d.ts +285 -0
- package/dist/sdk-hooks.js +31 -0
- package/dist/stores.d.ts +17 -0
- package/dist/stores.js +76 -0
- package/dist/tool-call-feed-Bs3MyQMT.d.ts +68 -0
- package/dist/tool-display-z4JcDmMQ.d.ts +32 -0
- package/dist/tool-previews.d.ts +48 -0
- package/dist/tool-previews.js +21 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +45 -0
- package/dist/utils.js +32 -0
- package/package.json +193 -0
- package/src/auth/auth.tsx +228 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/login-layout.tsx +46 -0
- package/src/chat/agent-timeline.stories.tsx +429 -0
- package/src/chat/agent-timeline.tsx +360 -0
- package/src/chat/chat-container.tsx +486 -0
- package/src/chat/chat-input.stories.tsx +142 -0
- package/src/chat/chat-input.tsx +389 -0
- package/src/chat/chat-message.stories.tsx +237 -0
- package/src/chat/chat-message.tsx +129 -0
- package/src/chat/index.ts +18 -0
- package/src/chat/message-list.stories.tsx +336 -0
- package/src/chat/message-list.tsx +79 -0
- package/src/chat/thinking-indicator.stories.tsx +56 -0
- package/src/chat/thinking-indicator.tsx +30 -0
- package/src/chat/user-message.stories.tsx +92 -0
- package/src/chat/user-message.tsx +43 -0
- package/src/editor/document-editor-pane.tsx +351 -0
- package/src/editor/editor-provider.tsx +428 -0
- package/src/editor/editor-toolbar.tsx +130 -0
- package/src/editor/index.ts +31 -0
- package/src/editor/markdown-conversion.ts +21 -0
- package/src/editor/markdown-document-editor.tsx +137 -0
- package/src/editor/tiptap-editor.tsx +331 -0
- package/src/editor/use-editor.ts +221 -0
- package/src/files/file-artifact-pane.tsx +183 -0
- package/src/files/file-preview.tsx +342 -0
- package/src/files/file-tabs.tsx +71 -0
- package/src/files/file-tree.tsx +258 -0
- package/src/files/index.ts +17 -0
- package/src/files/rich-file-tree.stories.tsx +104 -0
- package/src/files/rich-file-tree.test.tsx +42 -0
- package/src/files/rich-file-tree.tsx +232 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-auth.ts +153 -0
- package/src/hooks/use-auto-scroll.ts +59 -0
- package/src/hooks/use-dropdown-menu.ts +40 -0
- package/src/hooks/use-live-time.test.tsx +40 -0
- package/src/hooks/use-live-time.ts +27 -0
- package/src/hooks/use-realtime-session.ts +319 -0
- package/src/hooks/use-run-collapse-state.ts +25 -0
- package/src/hooks/use-run-groups.ts +111 -0
- package/src/hooks/use-sdk-session.ts +575 -0
- package/src/hooks/use-sse-stream.ts +475 -0
- package/src/hooks/use-tool-call-stream.ts +96 -0
- package/src/index.ts +14 -0
- package/src/lib/utils.ts +6 -0
- package/src/markdown/code-block.tsx +198 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/markdown.stories.tsx +190 -0
- package/src/markdown/markdown.tsx +62 -0
- package/src/openui/index.ts +20 -0
- package/src/openui/openui-artifact-renderer.tsx +542 -0
- package/src/primitives/artifact-pane.tsx +91 -0
- package/src/primitives/avatar.stories.tsx +95 -0
- package/src/primitives/avatar.tsx +47 -0
- package/src/primitives/badge.stories.tsx +57 -0
- package/src/primitives/badge.tsx +97 -0
- package/src/primitives/button.stories.tsx +48 -0
- package/src/primitives/button.tsx +115 -0
- package/src/primitives/card.stories.tsx +53 -0
- package/src/primitives/card.tsx +98 -0
- package/src/primitives/code-block.stories.tsx +115 -0
- package/src/primitives/code-block.tsx +22 -0
- package/src/primitives/design-tokens.stories.tsx +162 -0
- package/src/primitives/dialog.stories.tsx +176 -0
- package/src/primitives/dialog.tsx +137 -0
- package/src/primitives/drop-zone.stories.tsx +123 -0
- package/src/primitives/drop-zone.tsx +131 -0
- package/src/primitives/dropdown-menu.stories.tsx +122 -0
- package/src/primitives/dropdown-menu.tsx +214 -0
- package/src/primitives/empty-state.stories.tsx +81 -0
- package/src/primitives/empty-state.tsx +40 -0
- package/src/primitives/index.ts +118 -0
- package/src/primitives/input.stories.tsx +113 -0
- package/src/primitives/input.tsx +136 -0
- package/src/primitives/label.stories.tsx +84 -0
- package/src/primitives/label.tsx +24 -0
- package/src/primitives/progress.stories.tsx +93 -0
- package/src/primitives/progress.tsx +50 -0
- package/src/primitives/segmented-control.test.tsx +328 -0
- package/src/primitives/segmented-control.tsx +154 -0
- package/src/primitives/select.stories.tsx +164 -0
- package/src/primitives/select.tsx +158 -0
- package/src/primitives/sidebar-drop-zone.stories.tsx +100 -0
- package/src/primitives/sidebar-drop-zone.tsx +149 -0
- package/src/primitives/skeleton.stories.tsx +79 -0
- package/src/primitives/skeleton.tsx +55 -0
- package/src/primitives/stat-card.stories.tsx +137 -0
- package/src/primitives/stat-card.tsx +97 -0
- package/src/primitives/switch.stories.tsx +85 -0
- package/src/primitives/switch.tsx +28 -0
- package/src/primitives/table.stories.tsx +170 -0
- package/src/primitives/table.tsx +116 -0
- package/src/primitives/tabs.stories.tsx +180 -0
- package/src/primitives/tabs.tsx +71 -0
- package/src/primitives/terminal-display.stories.tsx +191 -0
- package/src/primitives/terminal-display.tsx +189 -0
- package/src/primitives/theme-toggle.stories.tsx +32 -0
- package/src/primitives/theme-toggle.tsx +96 -0
- package/src/primitives/toast.stories.tsx +155 -0
- package/src/primitives/toast.tsx +190 -0
- package/src/primitives/upload-progress.stories.tsx +120 -0
- package/src/primitives/upload-progress.tsx +110 -0
- package/src/run/expanded-tool-detail.stories.tsx +182 -0
- package/src/run/expanded-tool-detail.tsx +186 -0
- package/src/run/index.ts +13 -0
- package/src/run/inline-thinking-item.stories.tsx +136 -0
- package/src/run/inline-thinking-item.tsx +120 -0
- package/src/run/inline-tool-item.stories.tsx +222 -0
- package/src/run/inline-tool-item.tsx +190 -0
- package/src/run/run-group.stories.tsx +322 -0
- package/src/run/run-group.tsx +569 -0
- package/src/run/run-item-primitives.tsx +17 -0
- package/src/run/tool-call-feed.stories.tsx +294 -0
- package/src/run/tool-call-feed.tsx +192 -0
- package/src/run/tool-call-step.stories.tsx +198 -0
- package/src/run/tool-call-step.tsx +240 -0
- package/src/sdk-hooks.ts +38 -0
- package/src/stores/active-sessions-store.ts +455 -0
- package/src/stores/chat-store.ts +43 -0
- package/src/stores/index.ts +2 -0
- package/src/tool-previews/command-preview.tsx +116 -0
- package/src/tool-previews/diff-preview.tsx +85 -0
- package/src/tool-previews/glob-results-preview.tsx +98 -0
- package/src/tool-previews/grep-results-preview.tsx +157 -0
- package/src/tool-previews/index.ts +22 -0
- package/src/tool-previews/preview-primitives.tsx +84 -0
- package/src/tool-previews/question-preview.tsx +101 -0
- package/src/tool-previews/web-search-preview.tsx +117 -0
- package/src/tool-previews/write-file-preview.tsx +80 -0
- package/src/types/branding.ts +11 -0
- package/src/types/index.ts +5 -0
- package/src/types/message.ts +13 -0
- package/src/types/parts.ts +51 -0
- package/src/types/run.ts +56 -0
- package/src/types/tool-display.ts +41 -0
- package/src/utils/copy-text.ts +30 -0
- package/src/utils/format.test.ts +43 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/index.ts +10 -0
- package/src/utils/time-ago.ts +9 -0
- package/src/utils/tool-display.ts +238 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { lazy, Suspense } from "react";
|
|
2
|
+
import { Download, X } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
type DocumentEditorBackend,
|
|
5
|
+
type DocumentEditorMode,
|
|
6
|
+
type DocumentEditorPaneCollaborationConfig,
|
|
7
|
+
} from "../editor/document-editor-pane";
|
|
8
|
+
import { ArtifactPane, type ArtifactPaneProps } from "../primitives/artifact-pane";
|
|
9
|
+
import { FilePreview, type FilePreviewProps } from "./file-preview";
|
|
10
|
+
import { FileTabs, type FileTabData } from "./file-tabs";
|
|
11
|
+
|
|
12
|
+
const LazyDocumentEditorPane = lazy(async () => {
|
|
13
|
+
const module = await import("../editor/document-editor-pane");
|
|
14
|
+
return { default: module.DocumentEditorPane };
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export interface FileArtifactPaneEditorOptions {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
mode?: DocumentEditorMode;
|
|
20
|
+
defaultMode?: DocumentEditorMode;
|
|
21
|
+
onModeChange?: (mode: DocumentEditorMode) => void;
|
|
22
|
+
backend?: DocumentEditorBackend;
|
|
23
|
+
collaboration?: DocumentEditorPaneCollaborationConfig;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
autoFocus?: boolean;
|
|
26
|
+
readOnly?: boolean;
|
|
27
|
+
saving?: boolean;
|
|
28
|
+
saveLabel?: string;
|
|
29
|
+
onChange?: (markdown: string) => void;
|
|
30
|
+
onSave?: (markdown: string) => Promise<void> | void;
|
|
31
|
+
previewClassName?: string;
|
|
32
|
+
editorClassName?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FileArtifactPaneProps extends Omit<FilePreviewProps, "className"> {
|
|
36
|
+
path?: string;
|
|
37
|
+
tabs?: FileTabData[];
|
|
38
|
+
activeTabId?: string;
|
|
39
|
+
onTabSelect?: (id: string) => void;
|
|
40
|
+
onTabClose?: (id: string) => void;
|
|
41
|
+
eyebrow?: ArtifactPaneProps["eyebrow"];
|
|
42
|
+
meta?: ArtifactPaneProps["meta"];
|
|
43
|
+
toolbar?: ArtifactPaneProps["toolbar"];
|
|
44
|
+
footer?: ArtifactPaneProps["footer"];
|
|
45
|
+
className?: string;
|
|
46
|
+
editor?: FileArtifactPaneEditorOptions;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* FileArtifactPane — opinionated artifact frame for file previews with tabs and
|
|
51
|
+
* header actions.
|
|
52
|
+
*/
|
|
53
|
+
export function FileArtifactPane({
|
|
54
|
+
filename,
|
|
55
|
+
content,
|
|
56
|
+
blobUrl,
|
|
57
|
+
mimeType,
|
|
58
|
+
onClose,
|
|
59
|
+
onDownload,
|
|
60
|
+
path,
|
|
61
|
+
tabs = [],
|
|
62
|
+
activeTabId,
|
|
63
|
+
onTabSelect,
|
|
64
|
+
onTabClose,
|
|
65
|
+
eyebrow = "Artifact",
|
|
66
|
+
meta,
|
|
67
|
+
toolbar,
|
|
68
|
+
footer,
|
|
69
|
+
className,
|
|
70
|
+
editor,
|
|
71
|
+
}: FileArtifactPaneProps) {
|
|
72
|
+
const showTabs = tabs.length > 0 && onTabSelect && onTabClose;
|
|
73
|
+
const paneTabs = showTabs ? (
|
|
74
|
+
<FileTabs
|
|
75
|
+
tabs={tabs}
|
|
76
|
+
activeId={activeTabId}
|
|
77
|
+
onSelect={onTabSelect}
|
|
78
|
+
onClose={onTabClose}
|
|
79
|
+
/>
|
|
80
|
+
) : undefined;
|
|
81
|
+
const isMarkdown =
|
|
82
|
+
mimeType === "text/markdown" ||
|
|
83
|
+
filename.toLowerCase().endsWith(".md") ||
|
|
84
|
+
path?.toLowerCase().endsWith(".md");
|
|
85
|
+
const isEditableMarkdown = isMarkdown && editor?.enabled;
|
|
86
|
+
const headerActions = (
|
|
87
|
+
<>
|
|
88
|
+
{onDownload && (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-label={`Download ${filename}`}
|
|
92
|
+
onClick={onDownload}
|
|
93
|
+
className="rounded-[var(--radius-sm)] p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
94
|
+
>
|
|
95
|
+
<Download className="h-4 w-4" />
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
{onClose && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
aria-label={`Close ${filename}`}
|
|
102
|
+
onClick={onClose}
|
|
103
|
+
className="rounded-[var(--radius-sm)] p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
|
104
|
+
>
|
|
105
|
+
<X className="h-4 w-4" />
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (isEditableMarkdown) {
|
|
112
|
+
return (
|
|
113
|
+
<Suspense
|
|
114
|
+
fallback={(
|
|
115
|
+
<ArtifactPane
|
|
116
|
+
eyebrow={eyebrow}
|
|
117
|
+
title={filename}
|
|
118
|
+
subtitle={path}
|
|
119
|
+
meta={meta}
|
|
120
|
+
toolbar={toolbar}
|
|
121
|
+
footer={footer}
|
|
122
|
+
className={className}
|
|
123
|
+
tabs={paneTabs}
|
|
124
|
+
headerActions={headerActions}
|
|
125
|
+
>
|
|
126
|
+
<div className="flex min-h-[12rem] items-center justify-center rounded-[var(--radius-lg)] border border-dashed border-border bg-muted text-sm text-muted-foreground">
|
|
127
|
+
Loading editor…
|
|
128
|
+
</div>
|
|
129
|
+
</ArtifactPane>
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<LazyDocumentEditorPane
|
|
133
|
+
eyebrow={eyebrow}
|
|
134
|
+
title={filename}
|
|
135
|
+
subtitle={path}
|
|
136
|
+
meta={meta}
|
|
137
|
+
toolbar={toolbar}
|
|
138
|
+
footer={footer}
|
|
139
|
+
className={className}
|
|
140
|
+
tabs={paneTabs}
|
|
141
|
+
headerActions={headerActions}
|
|
142
|
+
markdown={content ?? ""}
|
|
143
|
+
mode={editor.mode}
|
|
144
|
+
defaultMode={editor.defaultMode}
|
|
145
|
+
onModeChange={editor.onModeChange}
|
|
146
|
+
backend={editor.backend}
|
|
147
|
+
collaboration={editor.collaboration}
|
|
148
|
+
placeholder={editor.placeholder}
|
|
149
|
+
autoFocus={editor.autoFocus}
|
|
150
|
+
readOnly={editor.readOnly}
|
|
151
|
+
saving={editor.saving}
|
|
152
|
+
saveLabel={editor.saveLabel}
|
|
153
|
+
onChange={editor.onChange}
|
|
154
|
+
onSave={editor.onSave}
|
|
155
|
+
previewClassName={editor.previewClassName}
|
|
156
|
+
editorClassName={editor.editorClassName}
|
|
157
|
+
/>
|
|
158
|
+
</Suspense>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<ArtifactPane
|
|
164
|
+
eyebrow={eyebrow}
|
|
165
|
+
title={filename}
|
|
166
|
+
subtitle={path}
|
|
167
|
+
meta={meta}
|
|
168
|
+
toolbar={toolbar}
|
|
169
|
+
footer={footer}
|
|
170
|
+
className={className}
|
|
171
|
+
tabs={paneTabs}
|
|
172
|
+
headerActions={headerActions}
|
|
173
|
+
>
|
|
174
|
+
<FilePreview
|
|
175
|
+
filename={filename}
|
|
176
|
+
content={content}
|
|
177
|
+
blobUrl={blobUrl}
|
|
178
|
+
mimeType={mimeType}
|
|
179
|
+
hideHeader={true}
|
|
180
|
+
/>
|
|
181
|
+
</ArtifactPane>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FilePreview — universal file renderer.
|
|
3
|
+
*
|
|
4
|
+
* Renders any file type beautifully:
|
|
5
|
+
* - PDF: embedded viewer
|
|
6
|
+
* - CSV/XLSX: tabular preview
|
|
7
|
+
* - Code (py/json/yaml/ts/js): line-numbered viewer
|
|
8
|
+
* - Markdown: rendered prose
|
|
9
|
+
* - Images: inline display
|
|
10
|
+
* - Text: monospace preview
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Download,
|
|
15
|
+
X,
|
|
16
|
+
FileText,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
import { cn } from "../lib/utils";
|
|
19
|
+
import { Markdown } from "../markdown/markdown";
|
|
20
|
+
|
|
21
|
+
export interface FilePreviewProps {
|
|
22
|
+
filename: string;
|
|
23
|
+
content?: string;
|
|
24
|
+
blobUrl?: string;
|
|
25
|
+
mimeType?: string;
|
|
26
|
+
onClose?: () => void;
|
|
27
|
+
onDownload?: () => void;
|
|
28
|
+
hideHeader?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getPreviewType(filename: string, mimeType?: string): string {
|
|
33
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
34
|
+
if (mimeType?.startsWith("application/pdf") || ext === "pdf") return "pdf";
|
|
35
|
+
if (mimeType?.startsWith("image/") || ["png", "jpg", "jpeg", "gif", "svg", "webp"].includes(ext)) return "image";
|
|
36
|
+
if (["csv"].includes(ext)) return "csv";
|
|
37
|
+
if (["xlsx", "xls"].includes(ext)) return "spreadsheet";
|
|
38
|
+
if (["py", "ts", "js", "tsx", "jsx", "sh", "bash"].includes(ext) || ["profile", "bashrc", "bash_logout", "env", "gitignore"].includes(ext)) return "code";
|
|
39
|
+
if (["json"].includes(ext)) return "json";
|
|
40
|
+
if (["yaml", "yml"].includes(ext)) return "yaml";
|
|
41
|
+
if (["md", "markdown"].includes(ext)) return "markdown";
|
|
42
|
+
if (["txt", "log", "text"].includes(ext)) return "text";
|
|
43
|
+
|
|
44
|
+
// If we have no known extension but we do have a text plain content payload, fallback to text rather than "unknown"
|
|
45
|
+
if (mimeType?.startsWith("text/plain")) return "text";
|
|
46
|
+
|
|
47
|
+
return "unknown";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPreviewLabel(previewType: string) {
|
|
51
|
+
switch (previewType) {
|
|
52
|
+
case "pdf":
|
|
53
|
+
return "PDF";
|
|
54
|
+
case "image":
|
|
55
|
+
return "Image";
|
|
56
|
+
case "csv":
|
|
57
|
+
return "CSV";
|
|
58
|
+
case "spreadsheet":
|
|
59
|
+
return "Spreadsheet";
|
|
60
|
+
case "code":
|
|
61
|
+
return "Code";
|
|
62
|
+
case "json":
|
|
63
|
+
return "JSON";
|
|
64
|
+
case "yaml":
|
|
65
|
+
return "YAML";
|
|
66
|
+
case "markdown":
|
|
67
|
+
return "Markdown";
|
|
68
|
+
case "text":
|
|
69
|
+
return "Text";
|
|
70
|
+
default:
|
|
71
|
+
return "File";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function CodePreview({ content, filename }: { content: string; filename: string }) {
|
|
76
|
+
const lines = content.split("\n");
|
|
77
|
+
const language = filename.split(".").pop()?.toUpperCase() || "TXT";
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="relative bg-background rounded-[var(--radius-md)] border border-border overflow-hidden">
|
|
81
|
+
<div className="flex items-center justify-between gap-3 border-b border-border px-4 py-2.5">
|
|
82
|
+
<div className="flex gap-1.5">
|
|
83
|
+
<div className="w-3 h-3 rounded-full bg-[#FF5F57]" />
|
|
84
|
+
<div className="w-3 h-3 rounded-full bg-[#FEBC2E]" />
|
|
85
|
+
<div className="w-3 h-3 rounded-full bg-[#8E59FF]" />
|
|
86
|
+
</div>
|
|
87
|
+
<div className="ml-2 min-w-0 flex-1 truncate text-xs font-mono text-muted-foreground">
|
|
88
|
+
{filename}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="inline-flex items-center gap-2 rounded-[var(--radius-full)] border border-border bg-card px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
|
|
91
|
+
<span>{language}</span>
|
|
92
|
+
<span className="h-1 w-1 rounded-full bg-[var(--border-hover)]" />
|
|
93
|
+
<span>{lines.length} lines</span>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="overflow-auto max-h-[70vh]">
|
|
97
|
+
<table className="w-full">
|
|
98
|
+
<tbody>
|
|
99
|
+
{lines.map((line, i) => (
|
|
100
|
+
<tr key={i} className="hover:bg-accent">
|
|
101
|
+
<td className="text-right pr-4 pl-4 py-0 select-none text-muted-foreground text-xs font-mono w-10 align-top leading-[1.55]">
|
|
102
|
+
{i + 1}
|
|
103
|
+
</td>
|
|
104
|
+
<td className="pr-4 py-0 font-mono text-[13px] text-foreground leading-[1.55] whitespace-pre">
|
|
105
|
+
{line || " "}
|
|
106
|
+
</td>
|
|
107
|
+
</tr>
|
|
108
|
+
))}
|
|
109
|
+
</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseCsvRow(line: string) {
|
|
117
|
+
const cells: string[] = [];
|
|
118
|
+
let current = "";
|
|
119
|
+
let inQuotes = false;
|
|
120
|
+
|
|
121
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
122
|
+
const char = line[index];
|
|
123
|
+
const next = line[index + 1];
|
|
124
|
+
|
|
125
|
+
if (char === "\"") {
|
|
126
|
+
if (inQuotes && next === "\"") {
|
|
127
|
+
current += "\"";
|
|
128
|
+
index += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
inQuotes = !inQuotes;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (char === "," && !inQuotes) {
|
|
137
|
+
cells.push(current.trim());
|
|
138
|
+
current = "";
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
current += char;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cells.push(current.trim());
|
|
146
|
+
return cells;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function CsvPreview({ content }: { content: string }) {
|
|
150
|
+
const lines = content
|
|
151
|
+
.trim()
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
|
|
155
|
+
if (lines.length === 0) return null;
|
|
156
|
+
|
|
157
|
+
const headers = parseCsvRow(lines[0]).map((header) => header.replace(/^"|"$/g, ""));
|
|
158
|
+
const rows = lines.slice(1).map((line) =>
|
|
159
|
+
parseCsvRow(line).map((cell) => cell.replace(/^"|"$/g, "")),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className="overflow-auto max-h-[70vh] rounded-[var(--radius-md)] border border-border">
|
|
164
|
+
<table className="w-full text-sm">
|
|
165
|
+
<thead>
|
|
166
|
+
<tr className="bg-muted/50 sticky top-0">
|
|
167
|
+
{headers.map((h, i) => (
|
|
168
|
+
<th
|
|
169
|
+
key={i}
|
|
170
|
+
className="px-3 py-2 text-left text-xs font-semibold text-foreground border-b border-border whitespace-nowrap"
|
|
171
|
+
>
|
|
172
|
+
{h}
|
|
173
|
+
</th>
|
|
174
|
+
))}
|
|
175
|
+
</tr>
|
|
176
|
+
</thead>
|
|
177
|
+
<tbody>
|
|
178
|
+
{rows.map((row, i) => (
|
|
179
|
+
<tr key={i} className="border-b border-border hover:bg-accent">
|
|
180
|
+
{row.map((cell, j) => (
|
|
181
|
+
<td
|
|
182
|
+
key={j}
|
|
183
|
+
className="px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap"
|
|
184
|
+
>
|
|
185
|
+
{cell}
|
|
186
|
+
</td>
|
|
187
|
+
))}
|
|
188
|
+
</tr>
|
|
189
|
+
))}
|
|
190
|
+
</tbody>
|
|
191
|
+
</table>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function ImagePreview({ src, filename }: { src: string; filename: string }) {
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex items-center justify-center p-4 bg-background rounded-[var(--radius-md)] border border-border">
|
|
199
|
+
<img src={src} alt={filename} className="max-w-full max-h-[70vh] object-contain rounded" />
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function PdfPreview({ blobUrl, filename }: { blobUrl: string; filename: string }) {
|
|
205
|
+
// Simple iframe-based PDF viewer. For richer rendering, consumers can
|
|
206
|
+
// swap in react-pdf at the app level.
|
|
207
|
+
return (
|
|
208
|
+
<div className="rounded-[var(--radius-md)] border border-border overflow-hidden bg-background">
|
|
209
|
+
<iframe
|
|
210
|
+
src={blobUrl}
|
|
211
|
+
title={filename}
|
|
212
|
+
className="w-full h-[70vh] border-0"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function TextPreview({ content }: { content: string }) {
|
|
219
|
+
return (
|
|
220
|
+
<pre className="bg-background rounded-[var(--radius-md)] border border-border p-4 overflow-auto max-h-[70vh] text-sm text-foreground font-mono leading-[1.55]">
|
|
221
|
+
{content}
|
|
222
|
+
</pre>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function UnsupportedPreview({
|
|
227
|
+
filename,
|
|
228
|
+
title,
|
|
229
|
+
description,
|
|
230
|
+
}: {
|
|
231
|
+
filename: string;
|
|
232
|
+
title: string;
|
|
233
|
+
description: string;
|
|
234
|
+
}) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="flex flex-col items-center justify-center rounded-[var(--radius-md)] border border-dashed border-border bg-background px-6 py-16 text-center text-muted-foreground">
|
|
237
|
+
<FileText className="mb-3 h-12 w-12 opacity-30" />
|
|
238
|
+
<p className="text-sm text-foreground">{title}</p>
|
|
239
|
+
<p className="mt-1 max-w-md text-xs">{description}</p>
|
|
240
|
+
<p className="mt-4 text-[11px] uppercase tracking-[0.12em]">{filename}</p>
|
|
241
|
+
</div>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function MarkdownPreview({ content }: { content: string }) {
|
|
246
|
+
return (
|
|
247
|
+
<div className="rounded-[var(--radius-md)] border border-border bg-background p-5">
|
|
248
|
+
<Markdown className="prose-sm max-w-none">{content}</Markdown>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function EmptyPreview({ filename }: { filename: string }) {
|
|
254
|
+
return (
|
|
255
|
+
<div className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
|
256
|
+
<FileText className="h-12 w-12 mb-3 opacity-30" />
|
|
257
|
+
<p className="text-sm">Cannot preview {filename}</p>
|
|
258
|
+
<p className="text-xs mt-1">Download to view this file</p>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function FilePreview({
|
|
264
|
+
filename,
|
|
265
|
+
content,
|
|
266
|
+
blobUrl,
|
|
267
|
+
mimeType,
|
|
268
|
+
onClose,
|
|
269
|
+
onDownload,
|
|
270
|
+
hideHeader = false,
|
|
271
|
+
className,
|
|
272
|
+
}: FilePreviewProps) {
|
|
273
|
+
const previewType = getPreviewType(filename, mimeType);
|
|
274
|
+
const previewLabel = getPreviewLabel(previewType);
|
|
275
|
+
const hasRenderableSource =
|
|
276
|
+
Boolean(content) ||
|
|
277
|
+
Boolean(blobUrl) ||
|
|
278
|
+
previewType === "unknown" ||
|
|
279
|
+
previewType === "spreadsheet";
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className={cn("flex flex-col h-full", className)}>
|
|
283
|
+
{!hideHeader && (
|
|
284
|
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-border shrink-0">
|
|
285
|
+
<div className="min-w-0 flex-1">
|
|
286
|
+
<div className="truncate text-sm font-medium text-foreground">{filename}</div>
|
|
287
|
+
<div className="mt-0.5 text-[11px] uppercase tracking-[0.12em] text-muted-foreground">
|
|
288
|
+
{previewLabel}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
{onDownload && (
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
onClick={onDownload}
|
|
295
|
+
aria-label={`Download ${filename}`}
|
|
296
|
+
className="p-1.5 rounded-[var(--radius-sm)] hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
297
|
+
>
|
|
298
|
+
<Download className="h-4 w-4" />
|
|
299
|
+
</button>
|
|
300
|
+
)}
|
|
301
|
+
{onClose && (
|
|
302
|
+
<button
|
|
303
|
+
type="button"
|
|
304
|
+
onClick={onClose}
|
|
305
|
+
aria-label={`Close ${filename}`}
|
|
306
|
+
className="p-1.5 rounded-[var(--radius-sm)] hover:bg-accent text-muted-foreground hover:text-foreground transition-colors"
|
|
307
|
+
>
|
|
308
|
+
<X className="h-4 w-4" />
|
|
309
|
+
</button>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
<div className="flex-1 overflow-auto p-3">
|
|
315
|
+
{previewType === "pdf" && blobUrl && <PdfPreview blobUrl={blobUrl} filename={filename} />}
|
|
316
|
+
{previewType === "image" && blobUrl && <ImagePreview src={blobUrl} filename={filename} />}
|
|
317
|
+
{previewType === "csv" && typeof content === "string" && <CsvPreview content={content} />}
|
|
318
|
+
{(previewType === "code" || previewType === "json" || previewType === "yaml") && typeof content === "string" && (
|
|
319
|
+
<CodePreview content={content} filename={filename} />
|
|
320
|
+
)}
|
|
321
|
+
{previewType === "text" && typeof content === "string" && <TextPreview content={content} />}
|
|
322
|
+
{previewType === "markdown" && typeof content === "string" && <MarkdownPreview content={content} />}
|
|
323
|
+
{previewType === "spreadsheet" && (
|
|
324
|
+
<UnsupportedPreview
|
|
325
|
+
filename={filename}
|
|
326
|
+
title="Spreadsheet preview is not available in this surface"
|
|
327
|
+
description="Download the workbook or convert it to CSV when you need an inline preview."
|
|
328
|
+
/>
|
|
329
|
+
)}
|
|
330
|
+
{previewType === "unknown" && typeof content !== "string" && <EmptyPreview filename={filename} />}
|
|
331
|
+
{previewType === "unknown" && typeof content === "string" && <TextPreview content={content} />}
|
|
332
|
+
{!hasRenderableSource && typeof content !== "string" && (
|
|
333
|
+
<UnsupportedPreview
|
|
334
|
+
filename={filename}
|
|
335
|
+
title="Preview data is not available yet"
|
|
336
|
+
description="This artifact can be shown once the app provides inline content or a downloadable blob."
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileTabs — tab bar for open files in the preview panel.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { X, FileText, FileCode, FileSpreadsheet } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
export interface FileTabData {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
dirty?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FileTabsProps {
|
|
16
|
+
tabs: FileTabData[];
|
|
17
|
+
activeId?: string;
|
|
18
|
+
onSelect: (id: string) => void;
|
|
19
|
+
onClose: (id: string) => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getTabIcon(name: string) {
|
|
24
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
25
|
+
if (["pdf"].includes(ext)) return FileText;
|
|
26
|
+
if (["csv", "xlsx"].includes(ext)) return FileSpreadsheet;
|
|
27
|
+
return FileCode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function FileTabs({ tabs, activeId, onSelect, onClose, className }: FileTabsProps) {
|
|
31
|
+
if (tabs.length === 0) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={cn("flex items-center border-b border-border bg-background overflow-x-auto", className)}>
|
|
35
|
+
{tabs.map((tab) => {
|
|
36
|
+
const isActive = tab.id === activeId;
|
|
37
|
+
const Icon = getTabIcon(tab.name);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
key={tab.id}
|
|
42
|
+
className={cn(
|
|
43
|
+
"group flex items-center border-r border-border shrink-0",
|
|
44
|
+
isActive
|
|
45
|
+
? "bg-card text-foreground border-b-2 border-b-primary"
|
|
46
|
+
: "text-muted-foreground hover:bg-muted/50",
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onSelect(tab.id)}
|
|
52
|
+
className="flex min-w-0 items-center gap-1.5 px-3 py-1.5 text-xs transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary/60"
|
|
53
|
+
>
|
|
54
|
+
<Icon className="h-3 w-3 shrink-0" />
|
|
55
|
+
<span className="max-w-[120px] truncate">{tab.name}</span>
|
|
56
|
+
{tab.dirty && <span className="h-1.5 w-1.5 rounded-full bg-primary" />}
|
|
57
|
+
</button>
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
aria-label={`Close ${tab.name}`}
|
|
61
|
+
onClick={() => onClose(tab.id)}
|
|
62
|
+
className="mr-1 rounded p-0.5 opacity-0 transition-opacity hover:bg-accent focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60 group-hover:opacity-100"
|
|
63
|
+
>
|
|
64
|
+
<X className="h-2.5 w-2.5" />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|