Skip to content

Commit be4a599

Browse files
committed
feat(ui): enhance artifact preview functionality with content display and language support
1 parent d7ee44c commit be4a599

7 files changed

Lines changed: 381 additions & 58 deletions

File tree

src/server/ui/api/models/disk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ export const getListArtifacts = async (
1414

1515
export const getArtifact = async (
1616
disk_id: string,
17-
file_path: string
17+
file_path: string,
18+
with_content: boolean = true
1819
): Promise<Res<GetArtifactResp>> => {
1920
return await service.get(
20-
`/api/disk/${disk_id}/artifact?file_path=${file_path}`
21+
`/api/disk/${disk_id}/artifact?file_path=${encodeURIComponent(file_path)}&with_content=${with_content}`
2122
);
2223
};
2324

src/server/ui/app/disk/page.tsx

Lines changed: 185 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,20 @@ import { Disk, ListArtifactsResp, Artifact as FileInfo } from "@/types";
4848
import ReactCodeMirror from "@uiw/react-codemirror";
4949
import { okaidia } from "@uiw/codemirror-theme-okaidia";
5050
import { json } from "@codemirror/lang-json";
51+
import { javascript } from "@codemirror/lang-javascript";
52+
import { python } from "@codemirror/lang-python";
53+
import { html } from "@codemirror/lang-html";
54+
import { css } from "@codemirror/lang-css";
55+
import { markdown } from "@codemirror/lang-markdown";
56+
import { xml } from "@codemirror/lang-xml";
57+
import { sql } from "@codemirror/lang-sql";
5158
import { EditorView } from "@codemirror/view";
59+
import { StreamLanguage } from "@codemirror/language";
60+
import { go } from "@codemirror/legacy-modes/mode/go";
61+
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
62+
import { shell } from "@codemirror/legacy-modes/mode/shell";
63+
import { rust } from "@codemirror/legacy-modes/mode/rust";
64+
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
5265

5366
interface TreeNode {
5467
id: string;
@@ -82,6 +95,79 @@ function truncateMiddle(str: string, maxLength: number = 30): string {
8295
);
8396
}
8497

98+
// Get language extension based on file type or filename
99+
function getLanguageExtension(contentType: string | null, filename?: string) {
100+
// First try to determine by content type
101+
if (contentType) {
102+
const type = contentType.toLowerCase();
103+
if (type.includes("json")) return json();
104+
if (type.includes("javascript") || type.includes("js")) return javascript();
105+
if (type.includes("typescript") || type.includes("ts"))
106+
return javascript({ typescript: true });
107+
if (type.includes("python") || type.includes("py")) return python();
108+
if (type.includes("html")) return html();
109+
if (type.includes("css")) return css();
110+
if (type.includes("markdown") || type.includes("md")) return markdown();
111+
if (type.includes("xml")) return xml();
112+
if (type.includes("sql")) return sql();
113+
if (type.includes("yaml") || type.includes("yml"))
114+
return StreamLanguage.define(yaml);
115+
if (type.includes("shell") || type.includes("bash") || type.includes("sh"))
116+
return StreamLanguage.define(shell);
117+
if (type.includes("go")) return StreamLanguage.define(go);
118+
if (type.includes("rust") || type.includes("rs"))
119+
return StreamLanguage.define(rust);
120+
if (type.includes("ruby") || type.includes("rb"))
121+
return StreamLanguage.define(ruby);
122+
}
123+
124+
// Then try to determine by filename extension
125+
if (filename) {
126+
const ext = filename.split(".").pop()?.toLowerCase();
127+
switch (ext) {
128+
case "json":
129+
return json();
130+
case "js":
131+
case "jsx":
132+
case "mjs":
133+
return javascript({ jsx: true });
134+
case "ts":
135+
case "tsx":
136+
return javascript({ typescript: true, jsx: ext === "tsx" });
137+
case "py":
138+
return python();
139+
case "html":
140+
case "htm":
141+
return html();
142+
case "css":
143+
return css();
144+
case "md":
145+
case "markdown":
146+
return markdown();
147+
case "xml":
148+
return xml();
149+
case "sql":
150+
return sql();
151+
case "yaml":
152+
case "yml":
153+
return StreamLanguage.define(yaml);
154+
case "sh":
155+
case "bash":
156+
case "zsh":
157+
return StreamLanguage.define(shell);
158+
case "go":
159+
return StreamLanguage.define(go);
160+
case "rs":
161+
return StreamLanguage.define(rust);
162+
case "rb":
163+
return StreamLanguage.define(ruby);
164+
}
165+
}
166+
167+
// Return empty array as fallback
168+
return [];
169+
}
170+
85171
function Node({
86172
node,
87173
style,
@@ -233,26 +319,27 @@ export default function DiskPage() {
233319

234320
// Disk related states
235321
const [disks, setDisks] = useState<Disk[]>([]);
236-
const [selectedDisk, setSelectedDisk] = useState<Disk | null>(
237-
null
238-
);
322+
const [selectedDisk, setSelectedDisk] = useState<Disk | null>(null);
239323
const [isLoadingDisks, setIsLoadingDisks] = useState(true);
240324

241325
// File preview states
242326
const [imageUrl, setImageUrl] = useState<string | null>(null);
327+
const [fileContent, setFileContent] = useState<string | null>(null);
328+
const [fileContentType, setFileContentType] = useState<string | null>(null);
243329
const [isLoadingPreview, setIsLoadingPreview] = useState(false);
244330
const [isLoadingDownload, setIsLoadingDownload] = useState(false);
245331

246332
// Delete confirmation dialog states
247333
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
248-
const [diskToDelete, setDiskToDelete] = useState<Disk | null>(
249-
null
250-
);
334+
const [diskToDelete, setDiskToDelete] = useState<Disk | null>(null);
251335
const [isDeleting, setIsDeleting] = useState(false);
252336

253337
// Delete artifact confirmation dialog states
254-
const [deleteArtifactDialogOpen, setDeleteArtifactDialogOpen] = useState(false);
255-
const [artifactToDelete, setArtifactToDelete] = useState<TreeNode | null>(null);
338+
const [deleteArtifactDialogOpen, setDeleteArtifactDialogOpen] =
339+
useState(false);
340+
const [artifactToDelete, setArtifactToDelete] = useState<TreeNode | null>(
341+
null
342+
);
256343
const [isDeletingArtifact, setIsDeletingArtifact] = useState(false);
257344

258345
// Upload artifact states
@@ -632,7 +719,8 @@ export default function DiskPage() {
632719

633720
// Handle delete file confirmation
634721
const handleDeleteArtifact = async () => {
635-
if (!artifactToDelete || !selectedDisk || !artifactToDelete.fileInfo) return;
722+
if (!artifactToDelete || !selectedDisk || !artifactToDelete.fileInfo)
723+
return;
636724

637725
try {
638726
setIsDeletingArtifact(true);
@@ -724,6 +812,8 @@ export default function DiskPage() {
724812
// Reset preview states when file selection changes
725813
useEffect(() => {
726814
setImageUrl(null);
815+
setFileContent(null);
816+
setFileContentType(null);
727817
}, [selectedFile]);
728818

729819
// Handle preview button click
@@ -734,13 +824,22 @@ export default function DiskPage() {
734824
setIsLoadingPreview(true);
735825
const res = await getArtifact(
736826
selectedDisk.id,
737-
`${selectedFile.path}${selectedFile.fileInfo.filename}`
827+
`${selectedFile.path}${selectedFile.fileInfo.filename}`,
828+
true // with_content
738829
);
739-
if (res.code !== 0) {
830+
if (res.code !== 0 || !res.data) {
740831
console.error(res.message);
741832
return;
742833
}
743-
setImageUrl(res.data?.public_url || null);
834+
835+
// Set image URL for image files
836+
setImageUrl(res.data.public_url || null);
837+
838+
// Set file content for text-based files
839+
if (res.data.content) {
840+
setFileContent(res.data.content.raw);
841+
setFileContentType(res.data.content.type);
842+
}
744843
} catch (error) {
745844
console.error("Failed to load preview:", error);
746845
} finally {
@@ -756,7 +855,8 @@ export default function DiskPage() {
756855
setIsLoadingDownload(true);
757856
const res = await getArtifact(
758857
selectedDisk.id,
759-
`${selectedFile.path}${selectedFile.fileInfo.filename}`
858+
`${selectedFile.path}${selectedFile.fileInfo.filename}`,
859+
false // with_content = false for download
760860
);
761861
if (res.code !== 0) {
762862
console.error(res.message);
@@ -1102,50 +1202,75 @@ export default function DiskPage() {
11021202
</div>
11031203
</div>
11041204

1105-
{/* Preview section for images */}
1106-
{selectedFile.fileInfo.meta.__artifact_info__.mime.startsWith(
1107-
"image/"
1108-
) && (
1109-
<div className="border-t pt-6">
1110-
<p className="text-sm font-medium text-muted-foreground mb-3">
1111-
{t("preview")}
1112-
</p>
1113-
{isLoadingPreview ? (
1114-
<div className="flex items-center justify-center h-64 bg-muted rounded-md">
1115-
<div className="flex flex-col items-center gap-2">
1116-
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
1117-
<p className="text-sm text-muted-foreground">
1118-
{t("loadingImage")}
1119-
</p>
1120-
</div>
1205+
{/* Preview section */}
1206+
<div className="border-t pt-6">
1207+
<p className="text-sm font-medium text-muted-foreground mb-3">
1208+
{t("preview")}
1209+
</p>
1210+
{isLoadingPreview ? (
1211+
<div className="flex items-center justify-center h-64 bg-muted rounded-md">
1212+
<div className="flex flex-col items-center gap-2">
1213+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
1214+
<p className="text-sm text-muted-foreground">
1215+
{t("loadingPreview")}
1216+
</p>
11211217
</div>
1122-
) : imageUrl ? (
1123-
<div className="rounded-md border bg-muted p-4">
1124-
<div className="relative w-full min-h-[200px]">
1125-
<Image
1126-
src={imageUrl}
1127-
alt={selectedFile.fileInfo.filename}
1128-
width={800}
1129-
height={600}
1130-
className="max-w-full h-auto rounded-md shadow-sm"
1131-
style={{ objectFit: "contain" }}
1132-
unoptimized
1218+
</div>
1219+
) : imageUrl || fileContent ? (
1220+
<>
1221+
{/* Image preview */}
1222+
{imageUrl &&
1223+
selectedFile.fileInfo.meta.__artifact_info__.mime.startsWith(
1224+
"image/"
1225+
) && (
1226+
<div className="rounded-md border bg-muted p-4 mb-4">
1227+
<div className="relative w-full min-h-[200px]">
1228+
<Image
1229+
src={imageUrl}
1230+
alt={selectedFile.fileInfo.filename}
1231+
width={800}
1232+
height={600}
1233+
className="max-w-full h-auto rounded-md shadow-sm"
1234+
style={{ objectFit: "contain" }}
1235+
unoptimized
1236+
/>
1237+
</div>
1238+
</div>
1239+
)}
1240+
1241+
{/* Text content preview */}
1242+
{fileContent && (
1243+
<div>
1244+
<ReactCodeMirror
1245+
value={fileContent}
1246+
height="400px"
1247+
theme={resolvedTheme === "dark" ? okaidia : "light"}
1248+
extensions={[
1249+
getLanguageExtension(
1250+
fileContentType,
1251+
selectedFile.fileInfo?.filename
1252+
),
1253+
EditorView.lineWrapping,
1254+
].flat()}
1255+
editable={false}
1256+
readOnly
1257+
className="border rounded-md overflow-hidden"
11331258
/>
11341259
</div>
1135-
</div>
1136-
) : (
1137-
<div className="flex items-center justify-center h-64 bg-muted rounded-md">
1138-
<Button
1139-
variant="outline"
1140-
onClick={handlePreviewClick}
1141-
disabled={isLoadingPreview}
1142-
>
1143-
{t("loadPreview")}
1144-
</Button>
1145-
</div>
1146-
)}
1147-
</div>
1148-
)}
1260+
)}
1261+
</>
1262+
) : (
1263+
<div className="flex items-center justify-center h-64 bg-muted rounded-md">
1264+
<Button
1265+
variant="outline"
1266+
onClick={handlePreviewClick}
1267+
disabled={isLoadingPreview}
1268+
>
1269+
{t("loadPreview")}
1270+
</Button>
1271+
</div>
1272+
)}
1273+
</div>
11491274
</div>
11501275
) : (
11511276
<p className="text-sm text-muted-foreground">
@@ -1285,7 +1410,9 @@ export default function DiskPage() {
12851410
className="border rounded-md overflow-hidden"
12861411
/>
12871412
{uploadMetaError && (
1288-
<p className="mt-2 text-sm text-destructive">{uploadMetaError}</p>
1413+
<p className="mt-2 text-sm text-destructive">
1414+
{uploadMetaError}
1415+
</p>
12891416
)}
12901417
<p className="text-xs text-muted-foreground mt-1">
12911418
{t("metaJsonHelp")}
@@ -1302,7 +1429,9 @@ export default function DiskPage() {
13021429
</AlertDialogCancel>
13031430
<AlertDialogAction
13041431
onClick={handleUploadConfirm}
1305-
disabled={isUploading || !selectedUploadFile || !isUploadMetaValid}
1432+
disabled={
1433+
isUploading || !selectedUploadFile || !isUploadMetaValid
1434+
}
13061435
>
13071436
{isUploading ? (
13081437
<>

src/server/ui/messages/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
"delete": "Delete",
3838
"preview": "Preview",
3939
"loadingImage": "Loading image...",
40+
"loadingPreview": "Loading preview...",
4041
"loadPreview": "Load Preview",
42+
"contentType": "Content Type",
4143
"selectFilePrompt": "Select an artifact from the tree to view its content",
4244
"deleteArtifactTitle": "Delete Disk",
4345
"deleteArtifactDescription": "Are you sure you want to delete disk",

src/server/ui/messages/zh.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
"delete": "删除",
3838
"preview": "预览",
3939
"loadingImage": "加载图片中...",
40+
"loadingPreview": "加载预览中...",
4041
"loadPreview": "加载预览",
42+
"contentType": "内容类型",
4143
"selectFilePrompt": "从树中选择一个工件以查看其内容",
4244
"deleteArtifactTitle": "删除磁盘",
4345
"deleteArtifactDescription": "确定要删除磁盘",

src/server/ui/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,16 @@
1414
"@blocknote/core": "^0.41.1",
1515
"@blocknote/react": "^0.41.1",
1616
"@blocknote/shadcn": "^0.41.1",
17+
"@codemirror/lang-css": "^6.3.1",
18+
"@codemirror/lang-html": "^6.4.11",
19+
"@codemirror/lang-javascript": "^6.2.4",
1720
"@codemirror/lang-json": "^6.0.2",
21+
"@codemirror/lang-markdown": "^6.4.0",
22+
"@codemirror/lang-python": "^6.2.1",
23+
"@codemirror/lang-sql": "^6.10.0",
24+
"@codemirror/lang-xml": "^6.1.0",
25+
"@codemirror/language": "^6.11.3",
26+
"@codemirror/legacy-modes": "^6.5.2",
1827
"@codemirror/view": "^6.38.6",
1928
"@radix-ui/react-alert-dialog": "^1.1.15",
2029
"@radix-ui/react-dialog": "^1.1.15",

0 commit comments

Comments
 (0)