@nextclaw/ui 0.15.2 → 0.15.4

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 (212) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-DxuPaDoK.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-eT8BF-8B.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-Cb2g0BOi.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-BcHjwdsb.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +14 -9
  61. package/src/app/components/layout/sidebar-items.tsx +3 -3
  62. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  63. package/src/app/components/layout/sidebar.tsx +1 -1
  64. package/src/app/styles/design-system.css +17 -4
  65. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  66. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  67. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  68. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  69. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  70. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  71. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  72. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  73. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  74. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  75. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  76. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -15
  77. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  78. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  79. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  80. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  81. package/src/features/chat/features/message/components/chat-message-list.container.tsx +63 -2
  82. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  85. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  86. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  88. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  89. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  93. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -5
  94. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +4 -4
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +4 -4
  96. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +463 -99
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +11 -5
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +8 -6
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +99 -67
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  106. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  107. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  108. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  109. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  110. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  111. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  112. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  113. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  114. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  115. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  116. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +21 -0
  117. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  118. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  119. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  121. package/src/features/chat/index.ts +1 -0
  122. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  123. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  124. package/src/features/chat/managers/chat-thread.manager.ts +123 -124
  125. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  127. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  128. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  129. package/src/features/chat/stores/chat-thread.store.ts +73 -13
  130. package/src/features/cron/components/cron-config.tsx +63 -80
  131. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  132. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  133. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  134. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  135. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  136. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  137. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  138. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  139. package/src/features/side-dock/components/side-dock.tsx +3 -3
  140. package/src/index.css +275 -15
  141. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  142. package/src/shared/components/actions/action-link.tsx +1 -1
  143. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  144. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  145. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  148. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  149. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  150. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  151. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  152. package/src/shared/components/file-type-icon.tsx +272 -0
  153. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  154. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  155. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  156. package/src/shared/components/ui/button.tsx +1 -1
  157. package/src/shared/components/ui/dialog.tsx +1 -1
  158. package/src/shared/components/ui/input.tsx +1 -1
  159. package/src/shared/components/ui/select.tsx +1 -1
  160. package/src/shared/components/ui/switch.tsx +1 -1
  161. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  162. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  163. package/src/shared/components/ui/tabs.tsx +1 -1
  164. package/src/shared/components/ui/textarea.tsx +1 -1
  165. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  166. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  167. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  168. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  169. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  170. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  171. package/dist/assets/api-B7Qigvtm.js +0 -13
  172. package/dist/assets/appearance-settings-page-CuE0Iiqj.js +0 -1
  173. package/dist/assets/channels-list-page-CTsM6TcP.js +0 -8
  174. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  175. package/dist/assets/config-split-page-Fbb4PtTf.js +0 -1
  176. package/dist/assets/confirm-dialog-D2EG3nqE.js +0 -5
  177. package/dist/assets/desktop-update-config-BGR3HUi1.js +0 -1
  178. package/dist/assets/dist-7JTjycxE.js +0 -1
  179. package/dist/assets/dist-wXLhk4hl.js +0 -1
  180. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  181. package/dist/assets/doc-browser-context-CYAGm5d5.js +0 -1
  182. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  183. package/dist/assets/es2015-j1SRCIX5.js +0 -41
  184. package/dist/assets/i18n-provider-B4oMxOFx.js +0 -1
  185. package/dist/assets/index-CvrvblgX.css +0 -1
  186. package/dist/assets/index-Obd4JYU2.js +0 -104
  187. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  189. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  190. package/dist/assets/model-config-page-DeKk9gvE.js +0 -1
  191. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  192. package/dist/assets/provider-scoped-model-input-DsH2LF1q.js +0 -1
  193. package/dist/assets/providers-config-page-BFnGp68F.js +0 -1
  194. package/dist/assets/react-UirzUuST.js +0 -8
  195. package/dist/assets/remote-CDkHZtA8.js +0 -1
  196. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  197. package/dist/assets/runtime-config-page-BelAqiNG.js +0 -1
  198. package/dist/assets/search-config-page-zPm6--Ig.js +0 -1
  199. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +0 -3
  200. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  201. package/dist/assets/select-CdSEOwVM.js +0 -1
  202. package/dist/assets/setting-row-CTAtGexr.js +0 -1
  203. package/dist/assets/side-dock-DPvakJ0a.js +0 -3
  204. package/dist/assets/switch-BU_UEULb.js +0 -1
  205. package/dist/assets/tag-chip-D-WsKoT1.js +0 -1
  206. package/dist/assets/tooltip-CjkOaHW7.js +0 -1
  207. package/dist/assets/use-config--hx5oxQn.js +0 -1
  208. package/dist/assets/use-confirm-dialog-CD9a_SK1.js +0 -1
  209. package/dist/assets/x-DT5x45JU.js +0 -1
  210. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  211. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  212. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -3,8 +3,14 @@ import { useQueryClient } from "@tanstack/react-query";
3
3
  import type { CronJobView } from "@/shared/lib/api";
4
4
  import { useNcpChildSessionTabsView } from "@/features/chat/features/ncp/hooks/use-ncp-child-session-tabs-view";
5
5
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
6
+ import {
7
+ CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
8
+ CHAT_WORKSPACE_PANEL_MAX_WIDTH,
9
+ CHAT_WORKSPACE_PANEL_MIN_WIDTH,
10
+ } from "@/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils";
6
11
  import type {
7
12
  ChatChildSessionTab,
13
+ ChatWorkspacePanelKind,
8
14
  ChatWorkspaceSideChatDraft,
9
15
  ChatWorkspaceNavigationEntry,
10
16
  ChatWorkspaceFileTab,
@@ -35,10 +41,11 @@ type ChatSessionWorkspacePanelProps = {
35
41
  activeWorkspaceFileKey: string | null;
36
42
  workspaceNavigationHistory?: readonly ChatWorkspaceNavigationEntry[];
37
43
  workspaceNavigationHistoryIndex?: number;
38
- activePanelKind?: "child-session" | "side-chat-draft" | "file" | "cron" | null;
44
+ activePanelKind?: ChatWorkspacePanelKind | null;
39
45
  sessionCronJobs?: readonly CronJobView[];
40
46
  sessionProjectRoot: string | null;
41
47
  sessionWorkingDir: string | null;
48
+ workspacePanelWidth?: number;
42
49
  displayMode?: "docked" | "overlay";
43
50
  };
44
51
 
@@ -55,6 +62,7 @@ export function ChatSessionWorkspacePanel({
55
62
  sessionCronJobs = [],
56
63
  sessionProjectRoot,
57
64
  sessionWorkingDir,
65
+ workspacePanelWidth = CHAT_WORKSPACE_PANEL_DEFAULT_WIDTH,
58
66
  displayMode = "docked",
59
67
  }: ChatSessionWorkspacePanelProps) {
60
68
  const presenter = usePresenter();
@@ -72,7 +80,6 @@ export function ChatSessionWorkspacePanel({
72
80
  activePanelKind,
73
81
  childSessionTabs: resolvedChildTabs,
74
82
  workspaceFileTabs,
75
- sessionCronJobCount: sessionCronJobs.length,
76
83
  });
77
84
  const workspaceHistory = {
78
85
  entries: workspaceNavigationHistory,
@@ -89,12 +96,24 @@ export function ChatSessionWorkspacePanel({
89
96
  resolvedChildTabs,
90
97
  activeSideChatDraft,
91
98
  workspaceFileTabs,
92
- sessionCronJobCount: sessionCronJobs.length,
93
99
  activeSelection,
94
100
  optimisticReadAtBySessionKey,
95
101
  onSelectSession: presenter.chatThreadManager.selectChildSessionDetail,
96
102
  onSelectFile: presenter.chatThreadManager.selectWorkspaceFile,
103
+ onOpenFileViewer: presenter.chatThreadManager.openWorkspaceFileViewer,
97
104
  onCloseFile: presenter.chatThreadManager.closeWorkspaceFile,
105
+ onSelectOverview: () => {
106
+ if (sessionKey)
107
+ presenter.chatThreadManager.openWorkspaceOverview(sessionKey);
108
+ },
109
+ onSelectChildSessions: () => {
110
+ if (sessionKey)
111
+ presenter.chatThreadManager.openChildSessions(sessionKey);
112
+ },
113
+ onSelectProjectFiles: () => {
114
+ if (sessionKey)
115
+ presenter.chatThreadManager.openProjectFiles(sessionKey);
116
+ },
98
117
  onSelectCronJobs: () => {
99
118
  if (sessionKey)
100
119
  presenter.chatThreadManager.openSessionCronPanel(sessionKey);
@@ -108,7 +127,6 @@ export function ChatSessionWorkspacePanel({
108
127
  resolvedChildTabs,
109
128
  sessionKey,
110
129
  workspaceFileTabs,
111
- sessionCronJobs.length,
112
130
  ],
113
131
  );
114
132
 
@@ -150,9 +168,11 @@ export function ChatSessionWorkspacePanel({
150
168
  : "hidden border-gray-200/70 bg-white/95 backdrop-blur-sm md:flex",
151
169
  isContainerMaximized ? "shadow-xl" : null,
152
170
  )}
153
- defaultWidth={480}
154
- minWidth={360}
155
- maxWidth={860}
171
+ defaultWidth={workspacePanelWidth}
172
+ width={workspacePanelWidth}
173
+ minWidth={CHAT_WORKSPACE_PANEL_MIN_WIDTH}
174
+ maxWidth={CHAT_WORKSPACE_PANEL_MAX_WIDTH}
175
+ onWidthCommit={presenter.chatThreadManager.setWorkspacePanelWidth}
156
176
  overlay={isOverlayPanel}
157
177
  overlayScope={isContainerMaximized ? "container" : "viewport"}
158
178
  >
@@ -175,7 +195,9 @@ export function ChatSessionWorkspacePanel({
175
195
  <div className="flex min-h-0 flex-1 flex-col bg-white">
176
196
  <ChatSessionWorkspacePanelContent
177
197
  activeSelection={activeSelection}
198
+ childSessionTabs={resolvedChildTabs}
178
199
  filePreviewRefreshVersion={filePreviewRefreshVersion}
200
+ sessionKey={sessionKey}
179
201
  sessionCronJobs={sessionCronJobs}
180
202
  sessionProjectRoot={sessionProjectRoot}
181
203
  sessionWorkingDir={sessionWorkingDir}
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ type WorkspaceFileBufferState =
4
+ | { contentUrl: string; status: "ready"; data: ArrayBuffer }
5
+ | { contentUrl: string; status: "error"; data: null };
6
+
7
+ export function useWorkspaceFileBuffer(contentUrl: string): {
8
+ status: "loading" | "ready" | "error";
9
+ data: ArrayBuffer | null;
10
+ } {
11
+ const [state, setState] = useState<WorkspaceFileBufferState | null>(null);
12
+ const currentState = state?.contentUrl === contentUrl ? state : null;
13
+
14
+ useEffect(() => {
15
+ const abortController = new AbortController();
16
+ void fetch(contentUrl, { signal: abortController.signal })
17
+ .then((response) => {
18
+ if (!response.ok) {
19
+ throw new Error(`Unable to load document (${response.status}).`);
20
+ }
21
+ return response.arrayBuffer();
22
+ })
23
+ .then((data) => {
24
+ if (!abortController.signal.aborted) {
25
+ setState({ contentUrl, status: "ready", data });
26
+ }
27
+ })
28
+ .catch((error: unknown) => {
29
+ if (!abortController.signal.aborted) {
30
+ console.error("Failed to load workspace document", error);
31
+ setState({ contentUrl, status: "error", data: null });
32
+ }
33
+ });
34
+
35
+ return () => abortController.abort();
36
+ }, [contentUrl]);
37
+
38
+ return {
39
+ status: currentState?.status ?? "loading",
40
+ data: currentState?.data ?? null,
41
+ };
42
+ }
@@ -0,0 +1,18 @@
1
+ import { t } from "@/shared/lib/i18n";
2
+
3
+ export function WorkspaceDocumentPreviewState({
4
+ status,
5
+ }: {
6
+ status: "loading" | "ready" | "error";
7
+ }) {
8
+ if (status === "ready") {
9
+ return null;
10
+ }
11
+ return (
12
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/90 px-6 text-center text-sm text-muted-foreground">
13
+ {status === "loading"
14
+ ? t("chatWorkspaceLoadingPreview")
15
+ : t("chatWorkspacePreviewFailed")}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,89 @@
1
+ .workspace-docx-preview {
2
+ container-type: inline-size;
3
+ }
4
+
5
+ .workspace-docx-preview .docx-wrapper {
6
+ gap: 1rem;
7
+ padding: 0 !important;
8
+ padding-bottom: 0 !important;
9
+ background: transparent !important;
10
+ }
11
+
12
+ .workspace-docx-preview .docx-wrapper > section.docx {
13
+ margin: 0 auto 1rem !important;
14
+ border: 1px solid rgb(0 0 0 / 8%);
15
+ border-radius: 4px;
16
+ box-shadow: 0 4px 18px rgb(0 0 0 / 12%) !important;
17
+ }
18
+
19
+ .workspace-docx-preview--reflow .docx-wrapper > section.docx {
20
+ width: 100% !important;
21
+ min-height: auto !important;
22
+ padding: clamp(1.5rem, 6cqw, 3.5rem) !important;
23
+ overflow: visible !important;
24
+ }
25
+
26
+ .workspace-docx-preview--reflow .docx-wrapper > section.docx > article {
27
+ min-width: 0;
28
+ }
29
+
30
+ .workspace-docx-preview--reflow .docx p {
31
+ line-height: 1.6 !important;
32
+ }
33
+
34
+ .workspace-docx-preview--reflow .docx p.docx_heading1 span {
35
+ min-height: 0 !important;
36
+ font-size: clamp(1.5rem, 6cqw, 2rem) !important;
37
+ line-height: 1.25 !important;
38
+ }
39
+
40
+ .workspace-docx-preview--reflow .docx p.docx_heading2 span {
41
+ min-height: 0 !important;
42
+ font-size: clamp(1.125rem, 4.5cqw, 1.375rem) !important;
43
+ line-height: 1.35 !important;
44
+ }
45
+
46
+ .workspace-docx-preview--reflow .docx table {
47
+ width: 100% !important;
48
+ margin: 0.75rem 0 1.25rem;
49
+ table-layout: auto !important;
50
+ }
51
+
52
+ .workspace-docx-preview--reflow .docx col {
53
+ width: auto !important;
54
+ }
55
+
56
+ .workspace-docx-preview--reflow .docx table td,
57
+ .workspace-docx-preview--reflow .docx table th {
58
+ padding: 0.45rem 0.55rem !important;
59
+ border-bottom: 1px solid rgb(0 0 0 / 9%);
60
+ }
61
+
62
+ .workspace-docx-preview--reflow .docx table tr:first-child > td,
63
+ .workspace-docx-preview--reflow .docx table tr:first-child > th {
64
+ background: rgb(0 0 0 / 4%);
65
+ font-weight: 600;
66
+ }
67
+
68
+ .workspace-docx-preview--reflow .docx table p {
69
+ min-height: 0;
70
+ margin: 0 !important;
71
+ }
72
+
73
+ .workspace-docx-preview--reflow .workspace-docx-wide-table {
74
+ max-width: 100%;
75
+ margin: 0.75rem 0 1.25rem;
76
+ overflow-x: auto;
77
+ scrollbar-width: thin;
78
+ }
79
+
80
+ .workspace-docx-preview--reflow .workspace-docx-wide-table table {
81
+ min-width: 36rem;
82
+ margin: 0;
83
+ font-size: 0.875rem;
84
+ }
85
+
86
+ .workspace-docx-preview--reflow .workspace-docx-wide-table td,
87
+ .workspace-docx-preview--reflow .workspace-docx-wide-table th {
88
+ white-space: nowrap;
89
+ }
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useWorkspaceFileBuffer } from "./use-workspace-file-buffer";
3
+ import { WorkspaceDocumentPreviewState } from "./workspace-document-preview-state";
4
+ import "./workspace-docx-preview.css";
5
+
6
+ function applyResponsiveDocxFallback(container: HTMLDivElement): void {
7
+ const pages = Array.from(
8
+ container.querySelectorAll<HTMLElement>(".docx-wrapper > section.docx"),
9
+ );
10
+ const needsFallback = pages.some(
11
+ (page) => !page.style.width && !page.style.padding,
12
+ );
13
+ container.classList.toggle("workspace-docx-preview--reflow", needsFallback);
14
+ if (!needsFallback) {
15
+ return;
16
+ }
17
+ for (const table of container.querySelectorAll<HTMLTableElement>("table")) {
18
+ if (table.querySelectorAll("col").length < 5) {
19
+ continue;
20
+ }
21
+ const scrollContainer = document.createElement("div");
22
+ scrollContainer.className = "workspace-docx-wide-table";
23
+ table.before(scrollContainer);
24
+ scrollContainer.append(table);
25
+ }
26
+ }
27
+
28
+ export function WorkspaceDocxPreview({ contentUrl }: { contentUrl: string }) {
29
+ const containerRef = useRef<HTMLDivElement | null>(null);
30
+ const fileBuffer = useWorkspaceFileBuffer(contentUrl);
31
+ const [renderState, setRenderState] = useState<{
32
+ contentUrl: string;
33
+ status: "ready" | "error";
34
+ } | null>(null);
35
+ const renderStatus =
36
+ renderState?.contentUrl === contentUrl ? renderState.status : "loading";
37
+ const status =
38
+ fileBuffer.status === "ready" ? renderStatus : fileBuffer.status;
39
+
40
+ useEffect(() => {
41
+ const container = containerRef.current;
42
+ if (!container || !fileBuffer.data) {
43
+ return undefined;
44
+ }
45
+ let disposed = false;
46
+ container.replaceChildren();
47
+ void import("docx-preview")
48
+ .then(async (docxPreview) => {
49
+ await docxPreview.renderAsync(
50
+ fileBuffer.data as ArrayBuffer,
51
+ container,
52
+ container,
53
+ {
54
+ breakPages: true,
55
+ inWrapper: true,
56
+ ignoreLastRenderedPageBreak: false,
57
+ },
58
+ );
59
+ if (!disposed) {
60
+ applyResponsiveDocxFallback(container);
61
+ setRenderState({ contentUrl, status: "ready" });
62
+ }
63
+ })
64
+ .catch((error: unknown) => {
65
+ if (!disposed) {
66
+ console.error("Failed to render DOCX preview", error);
67
+ setRenderState({ contentUrl, status: "error" });
68
+ }
69
+ });
70
+
71
+ return () => {
72
+ disposed = true;
73
+ container.replaceChildren();
74
+ };
75
+ }, [contentUrl, fileBuffer.data]);
76
+
77
+ return (
78
+ <div className="relative h-full overflow-auto bg-muted/40 p-4 custom-scrollbar">
79
+ <WorkspaceDocumentPreviewState status={status} />
80
+ <div
81
+ ref={containerRef}
82
+ className="workspace-docx-preview min-h-full"
83
+ data-testid="workspace-content-docx"
84
+ />
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,82 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { useWorkspaceFileBuffer } from "./use-workspace-file-buffer";
3
+ import { WorkspaceDocumentPreviewState } from "./workspace-document-preview-state";
4
+
5
+ export function WorkspacePresentationPreview({
6
+ contentUrl,
7
+ }: {
8
+ contentUrl: string;
9
+ }) {
10
+ const containerRef = useRef<HTMLDivElement | null>(null);
11
+ const fileBuffer = useWorkspaceFileBuffer(contentUrl);
12
+ const [renderState, setRenderState] = useState<{
13
+ contentUrl: string;
14
+ status: "ready" | "error";
15
+ } | null>(null);
16
+ const renderStatus =
17
+ renderState?.contentUrl === contentUrl ? renderState.status : "loading";
18
+ const status =
19
+ fileBuffer.status === "ready" ? renderStatus : fileBuffer.status;
20
+
21
+ useEffect(() => {
22
+ const container = containerRef.current;
23
+ if (!container || !fileBuffer.data) {
24
+ return undefined;
25
+ }
26
+ const abortController = new AbortController();
27
+ let viewer: { destroy: () => void } | null = null;
28
+ container.replaceChildren();
29
+ void import("@aiden0z/pptx-renderer")
30
+ .then(async ({ PptxViewer, RECOMMENDED_ZIP_LIMITS }) => {
31
+ viewer = await PptxViewer.open(
32
+ fileBuffer.data as ArrayBuffer,
33
+ container,
34
+ {
35
+ renderMode: "list",
36
+ fitMode: "contain",
37
+ lazySlides: true,
38
+ lazyMedia: true,
39
+ pdfjs: false,
40
+ scrollContainer: container,
41
+ signal: abortController.signal,
42
+ zipLimits: RECOMMENDED_ZIP_LIMITS,
43
+ listOptions: {
44
+ windowed: true,
45
+ batchSize: 8,
46
+ initialSlides: 4,
47
+ overscanViewport: 1.5,
48
+ },
49
+ },
50
+ );
51
+ if (abortController.signal.aborted) {
52
+ viewer.destroy();
53
+ viewer = null;
54
+ return;
55
+ }
56
+ setRenderState({ contentUrl, status: "ready" });
57
+ })
58
+ .catch((error: unknown) => {
59
+ if (!abortController.signal.aborted) {
60
+ console.error("Failed to render presentation preview", error);
61
+ setRenderState({ contentUrl, status: "error" });
62
+ }
63
+ });
64
+
65
+ return () => {
66
+ abortController.abort();
67
+ viewer?.destroy();
68
+ container.replaceChildren();
69
+ };
70
+ }, [contentUrl, fileBuffer.data]);
71
+
72
+ return (
73
+ <div className="relative h-full bg-muted/40">
74
+ <WorkspaceDocumentPreviewState status={status} />
75
+ <div
76
+ ref={containerRef}
77
+ className="h-full overflow-auto p-4 custom-scrollbar"
78
+ data-testid="workspace-content-presentation"
79
+ />
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,180 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useWorkspaceFileBuffer } from "./use-workspace-file-buffer";
3
+ import { WorkspaceDocumentPreviewState } from "./workspace-document-preview-state";
4
+ import { t } from "@/shared/lib/i18n";
5
+ import { cn } from "@/shared/lib/utils";
6
+
7
+ const MAX_PREVIEW_ROWS = 500;
8
+ const MAX_PREVIEW_COLUMNS = 100;
9
+
10
+ type SpreadsheetSheet = {
11
+ name: string;
12
+ rows: string[][];
13
+ columnCount: number;
14
+ truncated: boolean;
15
+ };
16
+
17
+ type SpreadsheetState = {
18
+ contentUrl: string;
19
+ status: "ready" | "error";
20
+ sheets: SpreadsheetSheet[];
21
+ };
22
+
23
+ function spreadsheetColumnLabel(index: number): string {
24
+ let label = "";
25
+ let value = index + 1;
26
+ while (value > 0) {
27
+ value -= 1;
28
+ label = String.fromCharCode(65 + (value % 26)) + label;
29
+ value = Math.floor(value / 26);
30
+ }
31
+ return label;
32
+ }
33
+
34
+ export function WorkspaceSpreadsheetPreview({
35
+ contentUrl,
36
+ }: {
37
+ contentUrl: string;
38
+ }) {
39
+ const fileBuffer = useWorkspaceFileBuffer(contentUrl);
40
+ const [spreadsheetState, setSpreadsheetState] =
41
+ useState<SpreadsheetState | null>(null);
42
+ const [selectedSheetName, setSelectedSheetName] = useState("");
43
+ const currentState =
44
+ spreadsheetState?.contentUrl === contentUrl ? spreadsheetState : null;
45
+ const status =
46
+ fileBuffer.status === "ready"
47
+ ? (currentState?.status ?? "loading")
48
+ : fileBuffer.status;
49
+ const activeSheet =
50
+ currentState?.sheets.find((sheet) => sheet.name === selectedSheetName) ??
51
+ currentState?.sheets[0] ??
52
+ null;
53
+
54
+ useEffect(() => {
55
+ if (!fileBuffer.data) {
56
+ return undefined;
57
+ }
58
+ let disposed = false;
59
+ void import("xlsx")
60
+ .then((xlsx) => {
61
+ const workbook = xlsx.read(fileBuffer.data as ArrayBuffer, {
62
+ dense: true,
63
+ sheetRows: MAX_PREVIEW_ROWS + 1,
64
+ });
65
+ const sheets = workbook.SheetNames.map((name) => {
66
+ const worksheet = workbook.Sheets[name];
67
+ const allRows = worksheet
68
+ ? xlsx.utils.sheet_to_json<unknown[]>(worksheet, {
69
+ header: 1,
70
+ raw: false,
71
+ defval: "",
72
+ blankrows: false,
73
+ })
74
+ : [];
75
+ const rows = allRows
76
+ .slice(0, MAX_PREVIEW_ROWS)
77
+ .map((row) =>
78
+ row
79
+ .slice(0, MAX_PREVIEW_COLUMNS)
80
+ .map((cell) => String(cell ?? "")),
81
+ );
82
+ return {
83
+ name,
84
+ rows,
85
+ columnCount: Math.min(
86
+ MAX_PREVIEW_COLUMNS,
87
+ rows.reduce((maximum, row) => Math.max(maximum, row.length), 0),
88
+ ),
89
+ truncated:
90
+ allRows.length > MAX_PREVIEW_ROWS ||
91
+ allRows.some((row) => row.length > MAX_PREVIEW_COLUMNS),
92
+ };
93
+ });
94
+ if (!disposed) {
95
+ setSpreadsheetState({ contentUrl, status: "ready", sheets });
96
+ }
97
+ })
98
+ .catch((error: unknown) => {
99
+ if (!disposed) {
100
+ console.error("Failed to render spreadsheet preview", error);
101
+ setSpreadsheetState({ contentUrl, status: "error", sheets: [] });
102
+ }
103
+ });
104
+ return () => {
105
+ disposed = true;
106
+ };
107
+ }, [contentUrl, fileBuffer.data]);
108
+
109
+ return (
110
+ <div
111
+ className="relative flex h-full min-h-0 flex-col bg-white"
112
+ data-testid="workspace-content-spreadsheet"
113
+ >
114
+ <WorkspaceDocumentPreviewState status={status} />
115
+ {currentState && currentState.sheets.length > 1 ? (
116
+ <div className="flex shrink-0 gap-1 overflow-x-auto border-b border-border px-2 py-1.5 custom-scrollbar">
117
+ {currentState.sheets.map((sheet) => (
118
+ <button
119
+ key={sheet.name}
120
+ type="button"
121
+ className={cn(
122
+ "h-7 shrink-0 rounded-md px-2.5 text-xs transition-colors",
123
+ sheet.name === activeSheet?.name
124
+ ? "bg-accent font-medium text-accent-foreground"
125
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
126
+ )}
127
+ onClick={() => setSelectedSheetName(sheet.name)}
128
+ >
129
+ {sheet.name}
130
+ </button>
131
+ ))}
132
+ </div>
133
+ ) : null}
134
+ {activeSheet ? (
135
+ <div className="min-h-0 flex-1 overflow-auto custom-scrollbar">
136
+ <table className="min-w-full border-collapse text-xs text-foreground">
137
+ <thead className="sticky top-0 z-[1] bg-muted">
138
+ <tr>
139
+ <th className="sticky left-0 z-[2] h-7 min-w-10 border border-border bg-muted px-2" />
140
+ {Array.from({ length: activeSheet.columnCount }, (_, index) => (
141
+ <th
142
+ key={index}
143
+ className="h-7 min-w-24 border border-border px-2 text-center font-medium text-muted-foreground"
144
+ >
145
+ {spreadsheetColumnLabel(index)}
146
+ </th>
147
+ ))}
148
+ </tr>
149
+ </thead>
150
+ <tbody>
151
+ {activeSheet.rows.map((row, rowIndex) => (
152
+ <tr key={rowIndex}>
153
+ <th className="sticky left-0 min-w-10 border border-border bg-muted px-2 text-right font-normal text-muted-foreground">
154
+ {rowIndex + 1}
155
+ </th>
156
+ {Array.from(
157
+ { length: activeSheet.columnCount },
158
+ (_, columnIndex) => (
159
+ <td
160
+ key={columnIndex}
161
+ className="max-w-80 whitespace-pre-wrap border border-border px-2 py-1.5 align-top"
162
+ >
163
+ {row[columnIndex] ?? ""}
164
+ </td>
165
+ ),
166
+ )}
167
+ </tr>
168
+ ))}
169
+ </tbody>
170
+ </table>
171
+ </div>
172
+ ) : null}
173
+ {activeSheet?.truncated ? (
174
+ <div className="shrink-0 border-t border-border bg-muted/60 px-3 py-1.5 text-xs text-muted-foreground">
175
+ {t("chatWorkspaceSpreadsheetPreviewTruncated")}
176
+ </div>
177
+ ) : null}
178
+ </div>
179
+ );
180
+ }