@nextclaw/agent-chat-ui 0.3.7 → 0.3.8

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.
Files changed (2) hide show
  1. package/dist/index.js +191 -52
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2303,6 +2303,25 @@ const IMAGE_EXTENSIONS = new Set([
2303
2303
  "tiff",
2304
2304
  "webp"
2305
2305
  ]);
2306
+ const AUDIO_EXTENSIONS = new Set([
2307
+ "aac",
2308
+ "flac",
2309
+ "m4a",
2310
+ "mp3",
2311
+ "ogg",
2312
+ "opus",
2313
+ "wav",
2314
+ "weba"
2315
+ ]);
2316
+ const VIDEO_EXTENSIONS = new Set([
2317
+ "avi",
2318
+ "m4v",
2319
+ "mov",
2320
+ "mp4",
2321
+ "mkv",
2322
+ "webm",
2323
+ "wmv"
2324
+ ]);
2306
2325
  const DOCUMENT_EXTENSIONS = new Set([
2307
2326
  "doc",
2308
2327
  "docx",
@@ -2360,6 +2379,36 @@ function readFileExtension(label) {
2360
2379
  function isImageDataUrl(dataUrl) {
2361
2380
  return typeof dataUrl === "string" && /^data:image\//i.test(dataUrl.trim());
2362
2381
  }
2382
+ function inferMimeTypeFromExtension(extension) {
2383
+ if (IMAGE_EXTENSIONS.has(extension)) {
2384
+ if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
2385
+ if (extension === "svg") return "image/svg+xml";
2386
+ return `image/${extension}`;
2387
+ }
2388
+ if (AUDIO_EXTENSIONS.has(extension)) {
2389
+ if (extension === "mp3") return "audio/mpeg";
2390
+ if (extension === "m4a") return "audio/mp4";
2391
+ if (extension === "weba") return "audio/webm";
2392
+ return `audio/${extension}`;
2393
+ }
2394
+ if (VIDEO_EXTENSIONS.has(extension)) {
2395
+ if (extension === "mov") return "video/quicktime";
2396
+ if (extension === "m4v") return "video/mp4";
2397
+ return `video/${extension}`;
2398
+ }
2399
+ if (extension === "pdf") return "application/pdf";
2400
+ return null;
2401
+ }
2402
+ function inferMimeTypeFromFileName(fileName) {
2403
+ const extension = readFileExtension(fileName);
2404
+ if (!extension) return null;
2405
+ return inferMimeTypeFromExtension(extension);
2406
+ }
2407
+ function resolveRenderableMimeType(file) {
2408
+ const normalizedMimeType = file.mimeType.trim().toLowerCase();
2409
+ if (normalizedMimeType.length > 0 && normalizedMimeType !== "application/octet-stream") return normalizedMimeType;
2410
+ return inferMimeTypeFromFileName(file.label);
2411
+ }
2363
2412
  function isImageFileLike(file) {
2364
2413
  const normalizedMimeType = file.mimeType.trim().toLowerCase();
2365
2414
  const extension = readFileExtension(file.label);
@@ -2369,8 +2418,8 @@ function resolveFileCategory(label, mimeType) {
2369
2418
  const extension = readFileExtension(label);
2370
2419
  const normalizedMimeType = mimeType.toLowerCase();
2371
2420
  if (normalizedMimeType.startsWith("image/") || IMAGE_EXTENSIONS.has(extension)) return "image";
2372
- if (normalizedMimeType.startsWith("audio/")) return "audio";
2373
- if (normalizedMimeType.startsWith("video/")) return "video";
2421
+ if (normalizedMimeType.startsWith("audio/") || AUDIO_EXTENSIONS.has(extension)) return "audio";
2422
+ if (normalizedMimeType.startsWith("video/") || VIDEO_EXTENSIONS.has(extension)) return "video";
2374
2423
  if (normalizedMimeType.includes("pdf") || extension === "pdf") return "pdf";
2375
2424
  if (ARCHIVE_EXTENSIONS.has(extension) || /(zip|tar|gzip|rar|compressed|archive)/.test(normalizedMimeType)) return "archive";
2376
2425
  if (SHEET_EXTENSIONS.has(extension) || /(spreadsheet|sheet|excel|csv)/.test(normalizedMimeType)) return "sheet";
@@ -2402,6 +2451,18 @@ const DEFAULT_FILE_CATEGORY_LABELS = {
2402
2451
  sheet: "Spreadsheet",
2403
2452
  video: "Video"
2404
2453
  };
2454
+ const FILE_CATEGORY_ICONS = {
2455
+ archive: FileArchive,
2456
+ audio: FileAudio2,
2457
+ code: FileCode2,
2458
+ data: FileJson2,
2459
+ document: FileText,
2460
+ generic: File,
2461
+ image: FileImage,
2462
+ pdf: FileText,
2463
+ sheet: FileSpreadsheet,
2464
+ video: FileVideo2
2465
+ };
2405
2466
  function readFileCategoryLabel(category, texts) {
2406
2467
  return texts?.attachmentCategoryLabels?.[category] ?? DEFAULT_FILE_CATEGORY_LABELS[category];
2407
2468
  }
@@ -2417,20 +2478,17 @@ function renderActionPill(label, isUser, isInteractive) {
2417
2478
  children: label
2418
2479
  });
2419
2480
  }
2420
- function readFileCategoryIcon(category) {
2421
- if (category === "archive") return FileArchive;
2422
- if (category === "audio") return FileAudio2;
2423
- if (category === "code") return FileCode2;
2424
- if (category === "data") return FileJson2;
2425
- if (category === "pdf") return FileText;
2426
- if (category === "sheet") return FileSpreadsheet;
2427
- if (category === "image") return FileImage;
2428
- if (category === "video") return FileVideo2;
2429
- if (category === "document") return FileText;
2430
- return File;
2481
+ function renderActionLink(label, href, isUser) {
2482
+ return /* @__PURE__ */ jsx("a", {
2483
+ href,
2484
+ target: "_blank",
2485
+ rel: "noreferrer",
2486
+ className: cn("inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-medium transition duration-200", isUser ? "border-white/14 bg-white/12 text-white hover:border-white/20 hover:bg-white/16" : "border-slate-200/80 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50"),
2487
+ children: label
2488
+ });
2431
2489
  }
2432
2490
  function FileCategoryGlyph({ category, isUser }) {
2433
- const Icon = readFileCategoryIcon(category);
2491
+ const Icon = FILE_CATEGORY_ICONS[category];
2434
2492
  return /* @__PURE__ */ jsx("div", {
2435
2493
  className: cn("flex h-14 w-14 shrink-0 items-center justify-center rounded-[1rem] border", isUser ? "border-white/12 bg-white/10 text-white" : FILE_CATEGORY_TILE_CLASSES[category]),
2436
2494
  children: /* @__PURE__ */ jsx(Icon, {
@@ -2439,14 +2497,10 @@ function FileCategoryGlyph({ category, isUser }) {
2439
2497
  })
2440
2498
  });
2441
2499
  }
2442
- function ChatMessageFile({ file, isUser = false, texts }) {
2443
- const { category, extension, sizeLabel } = buildChatMessageFileMeta(file);
2444
- const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
2445
- const isInteractive = Boolean(file.dataUrl);
2446
- const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
2447
- const categoryLabel = readFileCategoryLabel(category, texts);
2448
- const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-white/12 bg-white/10 text-white" : "border-slate-200/80 bg-white/95 text-slate-900", isInteractive && (isUser ? "hover:border-white/18 hover:bg-white/13" : "hover:border-slate-300 hover:bg-white"));
2449
- if (renderAsImage && file.dataUrl) return /* @__PURE__ */ jsx("a", {
2500
+ function renderImagePreview(params) {
2501
+ const { file, categoryLabel, sizeLabel, isUser } = params;
2502
+ if (!file.dataUrl) return null;
2503
+ return /* @__PURE__ */ jsx("a", {
2450
2504
  href: file.dataUrl,
2451
2505
  target: "_blank",
2452
2506
  rel: "noreferrer",
@@ -2469,7 +2523,10 @@ function ChatMessageFile({ file, isUser = false, texts }) {
2469
2523
  })]
2470
2524
  })
2471
2525
  });
2472
- const content = /* @__PURE__ */ jsxs("div", {
2526
+ }
2527
+ function renderFileCardHeader(params) {
2528
+ const { category, file, categoryLabel, sizeLabel, isUser, action } = params;
2529
+ return /* @__PURE__ */ jsxs("div", {
2473
2530
  className: "flex items-center gap-3 p-3.5",
2474
2531
  children: [/* @__PURE__ */ jsx(FileCategoryGlyph, {
2475
2532
  category,
@@ -2484,11 +2541,85 @@ function ChatMessageFile({ file, isUser = false, texts }) {
2484
2541
  className: "truncate text-[15px] font-semibold leading-5",
2485
2542
  children: file.label
2486
2543
  }), renderMetaLine(categoryLabel, sizeLabel, isUser)]
2487
- }), renderActionPill(actionLabel, isUser, isInteractive)]
2544
+ }), action]
2488
2545
  })
2489
2546
  })]
2490
2547
  });
2491
- if (!isInteractive) return /* @__PURE__ */ jsx("div", {
2548
+ }
2549
+ function renderInlineMediaCard(params) {
2550
+ const { category, file, categoryLabel, sizeLabel, isUser, shellClasses, actionLabel } = params;
2551
+ if (!file.dataUrl || category !== "audio" && category !== "video") return null;
2552
+ const mediaMimeType = resolveRenderableMimeType(file);
2553
+ return /* @__PURE__ */ jsxs("div", {
2554
+ className: shellClasses,
2555
+ children: [renderFileCardHeader({
2556
+ category,
2557
+ file,
2558
+ categoryLabel,
2559
+ sizeLabel,
2560
+ isUser,
2561
+ action: renderActionLink(actionLabel, file.dataUrl, isUser)
2562
+ }), /* @__PURE__ */ jsx("div", {
2563
+ className: "px-3.5 pb-3.5",
2564
+ children: category === "audio" ? /* @__PURE__ */ jsx("audio", {
2565
+ controls: true,
2566
+ preload: "metadata",
2567
+ "aria-label": file.label,
2568
+ className: "block w-full",
2569
+ children: /* @__PURE__ */ jsx("source", {
2570
+ src: file.dataUrl,
2571
+ ...mediaMimeType ? { type: mediaMimeType } : {}
2572
+ })
2573
+ }) : /* @__PURE__ */ jsx("div", {
2574
+ className: cn("overflow-hidden rounded-[1rem] border", isUser ? "border-white/10 bg-slate-950/26" : "border-slate-200/80 bg-slate-100/80"),
2575
+ children: /* @__PURE__ */ jsx("video", {
2576
+ controls: true,
2577
+ preload: "metadata",
2578
+ playsInline: true,
2579
+ "aria-label": file.label,
2580
+ className: "block max-h-[28rem] w-full bg-black",
2581
+ children: /* @__PURE__ */ jsx("source", {
2582
+ src: file.dataUrl,
2583
+ ...mediaMimeType ? { type: mediaMimeType } : {}
2584
+ })
2585
+ })
2586
+ })
2587
+ })]
2588
+ });
2589
+ }
2590
+ function ChatMessageFile({ file, isUser = false, texts }) {
2591
+ const { category, sizeLabel } = buildChatMessageFileMeta(file);
2592
+ const renderAsImage = isImageFileLike(file) && Boolean(file.dataUrl);
2593
+ const renderAsAudio = category === "audio" && Boolean(file.dataUrl);
2594
+ const renderAsVideo = category === "video" && Boolean(file.dataUrl);
2595
+ const isInteractive = Boolean(file.dataUrl);
2596
+ const actionLabel = isInteractive ? texts?.attachmentOpenLabel ?? "Open" : texts?.attachmentAttachedLabel ?? "Attached";
2597
+ const categoryLabel = readFileCategoryLabel(category, texts);
2598
+ const shellClasses = cn("block overflow-hidden rounded-[1.25rem] border transition duration-200", isUser ? "border-white/12 bg-white/10 text-white" : "border-slate-200/80 bg-white/95 text-slate-900", isInteractive && (isUser ? "hover:border-white/18 hover:bg-white/13" : "hover:border-slate-300 hover:bg-white"));
2599
+ if (renderAsImage) return renderImagePreview({
2600
+ file,
2601
+ categoryLabel,
2602
+ sizeLabel,
2603
+ isUser
2604
+ });
2605
+ if (renderAsAudio || renderAsVideo) return renderInlineMediaCard({
2606
+ category,
2607
+ file,
2608
+ categoryLabel,
2609
+ sizeLabel,
2610
+ isUser,
2611
+ shellClasses,
2612
+ actionLabel
2613
+ });
2614
+ const content = renderFileCardHeader({
2615
+ category,
2616
+ file,
2617
+ categoryLabel,
2618
+ sizeLabel,
2619
+ isUser,
2620
+ action: renderActionPill(actionLabel, isUser, isInteractive)
2621
+ });
2622
+ if (!isInteractive || !file.dataUrl) return /* @__PURE__ */ jsx("div", {
2492
2623
  className: shellClasses,
2493
2624
  children: content
2494
2625
  });
@@ -2759,13 +2890,11 @@ function ToolCardHeaderSessionAction({ action, onAction }) {
2759
2890
  //#endregion
2760
2891
  //#region src/components/chat/ui/chat-message-list/tool-card/tool-card-file-operation-lines.tsx
2761
2892
  const FILE_TEXT_CLASS_NAME = "font-mono text-[11px] leading-5";
2762
- const FILE_ROW_CLASS_NAME = `h-5 ${FILE_TEXT_CLASS_NAME}`;
2893
+ const FILE_ROW_CLASS_NAME = `flex h-5 w-full ${FILE_TEXT_CLASS_NAME}`;
2763
2894
  const FILE_LINE_NUMBER_CELL_CLASS_NAME = "flex h-5 items-center justify-center px-2.5 tabular-nums select-none";
2764
- function formatLineNumber(value) {
2765
- return typeof value === "number" ? String(value) : "";
2766
- }
2767
2895
  function readVisibleLineNumber(line) {
2768
- return formatLineNumber(line.newLineNumber ?? line.oldLineNumber);
2896
+ const value = line.newLineNumber ?? line.oldLineNumber;
2897
+ return typeof value === "number" ? String(value) : "";
2769
2898
  }
2770
2899
  function readLineKey(prefix, line, index) {
2771
2900
  return `${prefix}-${index}-${line.oldLineNumber ?? "x"}-${line.newLineNumber ?? "x"}`;
@@ -2783,9 +2912,8 @@ function readLineNumberColumnWidth(block) {
2783
2912
  }, 0);
2784
2913
  return `${Math.max(6.5, Math.min(8, maxDigits + 3.5))}ch`;
2785
2914
  }
2786
- function buildRowTemplateColumns(params) {
2787
- if (!params.showLineNumbers) return;
2788
- return { gridTemplateColumns: `${params.lineNumberColumnWidth} minmax(0, 1fr)` };
2915
+ function readCodeColumnWidth(block) {
2916
+ return `calc(${block.lines.reduce((currentMax, line) => Math.max(currentMax, Math.max(1, line.text.length)), 1)}ch + 1.25rem)`;
2789
2917
  }
2790
2918
  function getLineNumberTone(line) {
2791
2919
  if (line.kind === "remove") return "border-r border-rose-200 bg-rose-50 text-rose-700";
@@ -2798,29 +2926,27 @@ function getCodeRowTone(line) {
2798
2926
  return "bg-white text-amber-950/80";
2799
2927
  }
2800
2928
  function FileOperationLineNumberCell({ layout, line, lineNumberColumnWidth }) {
2801
- const lineNumberCellClassName = layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0";
2802
2929
  return /* @__PURE__ */ jsx("span", {
2803
2930
  "data-file-line-number-cell": "true",
2804
- style: layout === "compact" ? { width: lineNumberColumnWidth } : void 0,
2805
- className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, lineNumberCellClassName, getLineNumberTone(line)),
2931
+ style: layout === "compact" ? {
2932
+ width: lineNumberColumnWidth,
2933
+ minWidth: lineNumberColumnWidth
2934
+ } : void 0,
2935
+ className: cn(FILE_LINE_NUMBER_CELL_CLASS_NAME, layout === "compact" ? "sticky left-0 z-[1]" : "w-full shrink-0", getLineNumberTone(line)),
2806
2936
  children: readVisibleLineNumber(line)
2807
2937
  });
2808
2938
  }
2809
2939
  function FileOperationCodeCell({ line }) {
2810
2940
  return /* @__PURE__ */ jsx("span", {
2811
2941
  "data-file-code-row": "true",
2812
- className: cn("block min-w-full whitespace-pre px-2.5", getCodeRowTone(line)),
2942
+ className: cn("block min-w-0 flex-1 whitespace-pre px-2.5", getCodeRowTone(line)),
2813
2943
  children: line.text || " "
2814
2944
  });
2815
2945
  }
2816
2946
  function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth }) {
2817
2947
  return /* @__PURE__ */ jsxs("div", {
2818
2948
  "data-file-line-row": "true",
2819
- className: cn("grid w-max min-w-full", FILE_ROW_CLASS_NAME),
2820
- style: buildRowTemplateColumns({
2821
- showLineNumbers,
2822
- lineNumberColumnWidth
2823
- }),
2949
+ className: FILE_ROW_CLASS_NAME,
2824
2950
  children: [showLineNumbers ? /* @__PURE__ */ jsx(FileOperationLineNumberCell, {
2825
2951
  layout: "compact",
2826
2952
  line,
@@ -2831,6 +2957,7 @@ function FileOperationLineRow({ line, showLineNumbers, lineNumberColumnWidth })
2831
2957
  function FileOperationWorkspaceSurface({ block }) {
2832
2958
  const showLineNumbers = readHasBlockLineNumbers(block);
2833
2959
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
2960
+ const codeColumnWidth = readCodeColumnWidth(block);
2834
2961
  return /* @__PURE__ */ jsxs("div", {
2835
2962
  "data-file-code-surface": "true",
2836
2963
  "data-file-code-surface-layout": "workspace",
@@ -2850,11 +2977,16 @@ function FileOperationWorkspaceSurface({ block }) {
2850
2977
  }) : null, /* @__PURE__ */ jsxs("div", {
2851
2978
  "data-file-code-canvas": "true",
2852
2979
  className: "flex h-full min-h-full min-w-0 flex-1 flex-col bg-white",
2853
- children: [block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
2854
- "data-file-code-canvas-row": "true",
2855
- className: FILE_ROW_CLASS_NAME,
2856
- children: /* @__PURE__ */ jsx(FileOperationCodeCell, { line })
2857
- }, readLineKey("code", line, index))), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-white" })]
2980
+ children: [/* @__PURE__ */ jsx("div", {
2981
+ "data-file-code-stack": "true",
2982
+ className: "min-w-full",
2983
+ style: { minWidth: codeColumnWidth },
2984
+ children: block.lines.map((line, index) => /* @__PURE__ */ jsx("div", {
2985
+ "data-file-code-canvas-row": "true",
2986
+ className: FILE_ROW_CLASS_NAME,
2987
+ children: /* @__PURE__ */ jsx(FileOperationCodeCell, { line })
2988
+ }, readLineKey("code", line, index)))
2989
+ }), /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 bg-white" })]
2858
2990
  })]
2859
2991
  });
2860
2992
  }
@@ -2862,15 +2994,22 @@ function FileOperationCodeSurface({ block, layout = "compact" }) {
2862
2994
  if (layout === "workspace") return /* @__PURE__ */ jsx(FileOperationWorkspaceSurface, { block });
2863
2995
  const showLineNumbers = readHasBlockLineNumbers(block);
2864
2996
  const lineNumberColumnWidth = readLineNumberColumnWidth(block);
2997
+ const codeColumnWidth = readCodeColumnWidth(block);
2998
+ const surfaceMinWidth = showLineNumbers ? `calc(${lineNumberColumnWidth} + ${codeColumnWidth})` : codeColumnWidth;
2865
2999
  return /* @__PURE__ */ jsx("div", {
2866
3000
  "data-file-code-surface": "true",
2867
3001
  "data-file-code-surface-layout": "compact",
2868
3002
  className: "overflow-x-auto custom-scrollbar-amber bg-white",
2869
- children: block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineRow, {
2870
- line,
2871
- showLineNumbers,
2872
- lineNumberColumnWidth
2873
- }, readLineKey("row", line, index)))
3003
+ children: /* @__PURE__ */ jsx("div", {
3004
+ "data-file-code-stack": "true",
3005
+ className: "min-w-full",
3006
+ style: { minWidth: surfaceMinWidth },
3007
+ children: block.lines.map((line, index) => /* @__PURE__ */ jsx(FileOperationLineRow, {
3008
+ line,
3009
+ showLineNumbers,
3010
+ lineNumberColumnWidth
3011
+ }, readLineKey("row", line, index)))
3012
+ })
2874
3013
  });
2875
3014
  }
2876
3015
  function FileOperationLinesGrid({ block }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/agent-chat-ui",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "private": false,
5
5
  "description": "Reusable Nextclaw agent chat UI primitives and default skin.",
6
6
  "type": "module",