@docyrus/ui-pro-ai-assistant 0.0.1

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 (308) hide show
  1. package/dist/index.js +26161 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +155 -0
  4. package/src/components/assistant-animations.tsx +29 -0
  5. package/src/components/assistant-dialogs.tsx +235 -0
  6. package/src/components/code-view.tsx +278 -0
  7. package/src/components/create-agent-task.tsx +104 -0
  8. package/src/components/create-new-work-version.tsx +30 -0
  9. package/src/components/extract-web.tsx +160 -0
  10. package/src/components/forward-to-agent.tsx +90 -0
  11. package/src/components/generate-chart.tsx +101 -0
  12. package/src/components/generative-action-button.tsx +122 -0
  13. package/src/components/generative-tool.tsx +685 -0
  14. package/src/components/generative-ui-object.tsx +210 -0
  15. package/src/components/input-area.tsx +1209 -0
  16. package/src/components/json-schema-layout.tsx +326 -0
  17. package/src/components/list-item-card.tsx +92 -0
  18. package/src/components/mermaid-diagram.tsx +192 -0
  19. package/src/components/preview-image.tsx +47 -0
  20. package/src/components/request-approval.tsx +48 -0
  21. package/src/components/request-user-input.tsx +270 -0
  22. package/src/components/search-web.tsx +319 -0
  23. package/src/components/sheet-command.tsx +88 -0
  24. package/src/components/shell-canvas.tsx +122 -0
  25. package/src/components/show-advanced-data-table.tsx +352 -0
  26. package/src/components/show-generated-content-options.tsx +93 -0
  27. package/src/components/show-people-cards.tsx +180 -0
  28. package/src/components/subagent-tool.tsx +180 -0
  29. package/src/components/text-editor-tool.tsx +328 -0
  30. package/src/components/work-canvas.tsx +88 -0
  31. package/src/components/work-card.tsx +42 -0
  32. package/src/declarations.d.ts +1 -0
  33. package/src/docy-assistant.tsx +1962 -0
  34. package/src/hooks/index.ts +7 -0
  35. package/src/hooks/use-assistant-api.ts +507 -0
  36. package/src/hooks/use-deployment-data.ts +162 -0
  37. package/src/hooks/use-project-state.ts +347 -0
  38. package/src/hooks/use-session-state.ts +207 -0
  39. package/src/hooks/use-speech-recognition.ts +137 -0
  40. package/src/hooks/use-ui-state.ts +185 -0
  41. package/src/hooks/use-works-state.ts +146 -0
  42. package/src/i18n/context.tsx +253 -0
  43. package/src/i18n/index.ts +19 -0
  44. package/src/i18n/locales/de.json +198 -0
  45. package/src/i18n/locales/el.json +198 -0
  46. package/src/i18n/locales/en.json +226 -0
  47. package/src/i18n/locales/es.json +198 -0
  48. package/src/i18n/locales/fr.json +198 -0
  49. package/src/i18n/locales/it.json +198 -0
  50. package/src/i18n/locales/pt.json +198 -0
  51. package/src/i18n/locales/sl.json +198 -0
  52. package/src/i18n/locales/tr.json +211 -0
  53. package/src/i18n/types.ts +23 -0
  54. package/src/i18n/use-translation.ts +17 -0
  55. package/src/index.ts +18 -0
  56. package/src/internal/plate-editor/editor/auth-context.ts +11 -0
  57. package/src/internal/plate-editor/editor/editor-base-kit.tsx +39 -0
  58. package/src/internal/plate-editor/editor/editor-kit.tsx +89 -0
  59. package/src/internal/plate-editor/editor/plate-editor.tsx +75 -0
  60. package/src/internal/plate-editor/editor/plate-types.ts +126 -0
  61. package/src/internal/plate-editor/editor/plugins/ai-kit.tsx +172 -0
  62. package/src/internal/plate-editor/editor/plugins/autoformat-kit.tsx +211 -0
  63. package/src/internal/plate-editor/editor/plugins/basic-blocks-base-kit.tsx +26 -0
  64. package/src/internal/plate-editor/editor/plugins/basic-blocks-kit.tsx +51 -0
  65. package/src/internal/plate-editor/editor/plugins/basic-marks-base-kit.tsx +24 -0
  66. package/src/internal/plate-editor/editor/plugins/basic-marks-kit.tsx +38 -0
  67. package/src/internal/plate-editor/editor/plugins/basic-nodes-kit.tsx +6 -0
  68. package/src/internal/plate-editor/editor/plugins/block-menu-kit.tsx +14 -0
  69. package/src/internal/plate-editor/editor/plugins/block-placeholder-kit.tsx +17 -0
  70. package/src/internal/plate-editor/editor/plugins/block-selection-kit.tsx +31 -0
  71. package/src/internal/plate-editor/editor/plugins/callout-base-kit.tsx +5 -0
  72. package/src/internal/plate-editor/editor/plugins/callout-kit.tsx +7 -0
  73. package/src/internal/plate-editor/editor/plugins/code-block-base-kit.tsx +23 -0
  74. package/src/internal/plate-editor/editor/plugins/code-block-kit.tsx +26 -0
  75. package/src/internal/plate-editor/editor/plugins/column-base-kit.tsx +8 -0
  76. package/src/internal/plate-editor/editor/plugins/column-kit.tsx +7 -0
  77. package/src/internal/plate-editor/editor/plugins/comment-base-kit.tsx +5 -0
  78. package/src/internal/plate-editor/editor/plugins/comment-kit.tsx +174 -0
  79. package/src/internal/plate-editor/editor/plugins/copilot-kit.tsx +68 -0
  80. package/src/internal/plate-editor/editor/plugins/cursor-overlay-kit.tsx +13 -0
  81. package/src/internal/plate-editor/editor/plugins/date-base-kit.tsx +5 -0
  82. package/src/internal/plate-editor/editor/plugins/date-kit.tsx +7 -0
  83. package/src/internal/plate-editor/editor/plugins/discussion-kit.tsx +36 -0
  84. package/src/internal/plate-editor/editor/plugins/dnd-kit.tsx +27 -0
  85. package/src/internal/plate-editor/editor/plugins/docx-export-kit.tsx +43 -0
  86. package/src/internal/plate-editor/editor/plugins/docx-kit.tsx +6 -0
  87. package/src/internal/plate-editor/editor/plugins/emoji-kit.tsx +15 -0
  88. package/src/internal/plate-editor/editor/plugins/exit-break-kit.tsx +12 -0
  89. package/src/internal/plate-editor/editor/plugins/floating-toolbar-kit.tsx +19 -0
  90. package/src/internal/plate-editor/editor/plugins/font-base-kit.tsx +36 -0
  91. package/src/internal/plate-editor/editor/plugins/font-kit.tsx +47 -0
  92. package/src/internal/plate-editor/editor/plugins/indent-base-kit.tsx +19 -0
  93. package/src/internal/plate-editor/editor/plugins/indent-kit.tsx +22 -0
  94. package/src/internal/plate-editor/editor/plugins/link-base-kit.tsx +5 -0
  95. package/src/internal/plate-editor/editor/plugins/link-kit.tsx +35 -0
  96. package/src/internal/plate-editor/editor/plugins/list-base-kit.tsx +24 -0
  97. package/src/internal/plate-editor/editor/plugins/list-kit.tsx +27 -0
  98. package/src/internal/plate-editor/editor/plugins/markdown-kit.tsx +18 -0
  99. package/src/internal/plate-editor/editor/plugins/math-base-kit.tsx +8 -0
  100. package/src/internal/plate-editor/editor/plugins/math-kit.tsx +10 -0
  101. package/src/internal/plate-editor/editor/plugins/media-base-kit.tsx +37 -0
  102. package/src/internal/plate-editor/editor/plugins/media-kit.tsx +53 -0
  103. package/src/internal/plate-editor/editor/plugins/mention-base-kit.tsx +5 -0
  104. package/src/internal/plate-editor/editor/plugins/mention-kit.tsx +36 -0
  105. package/src/internal/plate-editor/editor/plugins/slash-kit.tsx +17 -0
  106. package/src/internal/plate-editor/editor/plugins/suggestion-base-kit.tsx +5 -0
  107. package/src/internal/plate-editor/editor/plugins/suggestion-kit.tsx +95 -0
  108. package/src/internal/plate-editor/editor/plugins/table-base-kit.tsx +20 -0
  109. package/src/internal/plate-editor/editor/plugins/table-kit.tsx +22 -0
  110. package/src/internal/plate-editor/editor/plugins/toc-base-kit.tsx +5 -0
  111. package/src/internal/plate-editor/editor/plugins/toc-kit.tsx +14 -0
  112. package/src/internal/plate-editor/editor/plugins/toggle-base-kit.tsx +5 -0
  113. package/src/internal/plate-editor/editor/plugins/toggle-kit.tsx +9 -0
  114. package/src/internal/plate-editor/editor/transforms.ts +165 -0
  115. package/src/internal/plate-editor/editor/use-chat.ts +152 -0
  116. package/src/internal/plate-editor/hooks/index.ts +3 -0
  117. package/src/internal/plate-editor/hooks/use-copy-to-clipboard.ts +31 -0
  118. package/src/internal/plate-editor/hooks/use-is-touch-device.ts +26 -0
  119. package/src/internal/plate-editor/hooks/use-lock-scroll.ts +21 -0
  120. package/src/internal/plate-editor/hooks/use-media-query.ts +44 -0
  121. package/src/internal/plate-editor/hooks/use-mounted.ts +18 -0
  122. package/src/internal/plate-editor/hooks/use-on-click-outside.ts +114 -0
  123. package/src/internal/plate-editor/hooks/use-upload-file.ts +81 -0
  124. package/src/internal/plate-editor/i18n/context.tsx +58 -0
  125. package/src/internal/plate-editor/i18n/index.ts +3 -0
  126. package/src/internal/plate-editor/i18n/locales/de.json +57 -0
  127. package/src/internal/plate-editor/i18n/locales/el.json +57 -0
  128. package/src/internal/plate-editor/i18n/locales/en.json +57 -0
  129. package/src/internal/plate-editor/i18n/locales/es.json +57 -0
  130. package/src/internal/plate-editor/i18n/locales/fr.json +57 -0
  131. package/src/internal/plate-editor/i18n/locales/it.json +57 -0
  132. package/src/internal/plate-editor/i18n/locales/pt.json +57 -0
  133. package/src/internal/plate-editor/i18n/locales/sl.json +57 -0
  134. package/src/internal/plate-editor/i18n/locales/tr.json +57 -0
  135. package/src/internal/plate-editor/i18n/types.ts +59 -0
  136. package/src/internal/plate-editor/i18n/use-translation.ts +22 -0
  137. package/src/internal/plate-editor/index.ts +39 -0
  138. package/src/internal/plate-editor/lib/ai-output-converter.ts +153 -0
  139. package/src/internal/plate-editor/lib/download-file.ts +17 -0
  140. package/src/internal/plate-editor/plate-ui/ai-chat-editor.tsx +26 -0
  141. package/src/internal/plate-editor/plate-ui/ai-menu.tsx +828 -0
  142. package/src/internal/plate-editor/plate-ui/ai-node.tsx +41 -0
  143. package/src/internal/plate-editor/plate-ui/ai-toolbar-button.tsx +27 -0
  144. package/src/internal/plate-editor/plate-ui/alert-dialog.tsx +147 -0
  145. package/src/internal/plate-editor/plate-ui/align-toolbar-button.tsx +90 -0
  146. package/src/internal/plate-editor/plate-ui/avatar.tsx +3 -0
  147. package/src/internal/plate-editor/plate-ui/block-context-menu.tsx +106 -0
  148. package/src/internal/plate-editor/plate-ui/block-discussion.tsx +364 -0
  149. package/src/internal/plate-editor/plate-ui/block-draggable.tsx +556 -0
  150. package/src/internal/plate-editor/plate-ui/block-list-static.tsx +78 -0
  151. package/src/internal/plate-editor/plate-ui/block-list.tsx +85 -0
  152. package/src/internal/plate-editor/plate-ui/block-menu.tsx +557 -0
  153. package/src/internal/plate-editor/plate-ui/block-selection.tsx +47 -0
  154. package/src/internal/plate-editor/plate-ui/block-suggestion.tsx +469 -0
  155. package/src/internal/plate-editor/plate-ui/blockquote-node-static.tsx +10 -0
  156. package/src/internal/plate-editor/plate-ui/blockquote-node.tsx +11 -0
  157. package/src/internal/plate-editor/plate-ui/button.tsx +190 -0
  158. package/src/internal/plate-editor/plate-ui/calendar.tsx +3 -0
  159. package/src/internal/plate-editor/plate-ui/callout-node-static.tsx +76 -0
  160. package/src/internal/plate-editor/plate-ui/callout-node.tsx +54 -0
  161. package/src/internal/plate-editor/plate-ui/caption.tsx +48 -0
  162. package/src/internal/plate-editor/plate-ui/checkbox.tsx +3 -0
  163. package/src/internal/plate-editor/plate-ui/code-block-node-static.tsx +172 -0
  164. package/src/internal/plate-editor/plate-ui/code-block-node.tsx +228 -0
  165. package/src/internal/plate-editor/plate-ui/code-node-static.tsx +11 -0
  166. package/src/internal/plate-editor/plate-ui/code-node.tsx +12 -0
  167. package/src/internal/plate-editor/plate-ui/column-node-static.tsx +65 -0
  168. package/src/internal/plate-editor/plate-ui/column-node.tsx +24 -0
  169. package/src/internal/plate-editor/plate-ui/command.tsx +204 -0
  170. package/src/internal/plate-editor/plate-ui/comment-node-static.tsx +12 -0
  171. package/src/internal/plate-editor/plate-ui/comment-node.tsx +45 -0
  172. package/src/internal/plate-editor/plate-ui/comment-toolbar-button.tsx +24 -0
  173. package/src/internal/plate-editor/plate-ui/comment.tsx +619 -0
  174. package/src/internal/plate-editor/plate-ui/cursor-overlay.tsx +85 -0
  175. package/src/internal/plate-editor/plate-ui/date-node-static.tsx +43 -0
  176. package/src/internal/plate-editor/plate-ui/date-node.tsx +56 -0
  177. package/src/internal/plate-editor/plate-ui/dialog.tsx +426 -0
  178. package/src/internal/plate-editor/plate-ui/dropdown-menu.tsx +266 -0
  179. package/src/internal/plate-editor/plate-ui/editor-static.tsx +40 -0
  180. package/src/internal/plate-editor/plate-ui/editor.tsx +148 -0
  181. package/src/internal/plate-editor/plate-ui/emoji-node.tsx +48 -0
  182. package/src/internal/plate-editor/plate-ui/emoji-toolbar-button.tsx +626 -0
  183. package/src/internal/plate-editor/plate-ui/equation-node-static.tsx +155 -0
  184. package/src/internal/plate-editor/plate-ui/equation-node.tsx +226 -0
  185. package/src/internal/plate-editor/plate-ui/equation-toolbar-button.tsx +26 -0
  186. package/src/internal/plate-editor/plate-ui/export-toolbar-button.tsx +208 -0
  187. package/src/internal/plate-editor/plate-ui/fixed-toolbar-buttons.tsx +157 -0
  188. package/src/internal/plate-editor/plate-ui/fixed-toolbar.tsx +27 -0
  189. package/src/internal/plate-editor/plate-ui/floating-discussion.tsx +1129 -0
  190. package/src/internal/plate-editor/plate-ui/floating-toolbar-buttons.tsx +129 -0
  191. package/src/internal/plate-editor/plate-ui/floating-toolbar.tsx +99 -0
  192. package/src/internal/plate-editor/plate-ui/font-color-toolbar-button.tsx +211 -0
  193. package/src/internal/plate-editor/plate-ui/font-size-toolbar-button.tsx +154 -0
  194. package/src/internal/plate-editor/plate-ui/ghost-text.tsx +20 -0
  195. package/src/internal/plate-editor/plate-ui/heading-node-static.tsx +52 -0
  196. package/src/internal/plate-editor/plate-ui/heading-node.tsx +56 -0
  197. package/src/internal/plate-editor/plate-ui/highlight-node-static.tsx +9 -0
  198. package/src/internal/plate-editor/plate-ui/highlight-node.tsx +11 -0
  199. package/src/internal/plate-editor/plate-ui/history-toolbar-button.tsx +52 -0
  200. package/src/internal/plate-editor/plate-ui/hover-card.tsx +7 -0
  201. package/src/internal/plate-editor/plate-ui/hr-node-static.tsx +18 -0
  202. package/src/internal/plate-editor/plate-ui/hr-node.tsx +28 -0
  203. package/src/internal/plate-editor/plate-ui/import-toolbar-button.tsx +124 -0
  204. package/src/internal/plate-editor/plate-ui/indent-toolbar-button.tsx +34 -0
  205. package/src/internal/plate-editor/plate-ui/inline-combobox.tsx +409 -0
  206. package/src/internal/plate-editor/plate-ui/input.tsx +39 -0
  207. package/src/internal/plate-editor/plate-ui/insert-toolbar-button.tsx +260 -0
  208. package/src/internal/plate-editor/plate-ui/label.tsx +1 -0
  209. package/src/internal/plate-editor/plate-ui/line-height-toolbar-button.tsx +71 -0
  210. package/src/internal/plate-editor/plate-ui/link-node-static.tsx +15 -0
  211. package/src/internal/plate-editor/plate-ui/link-node.tsx +33 -0
  212. package/src/internal/plate-editor/plate-ui/link-toolbar-button.tsx +30 -0
  213. package/src/internal/plate-editor/plate-ui/link-toolbar.tsx +149 -0
  214. package/src/internal/plate-editor/plate-ui/list-toolbar-button.tsx +179 -0
  215. package/src/internal/plate-editor/plate-ui/mark-toolbar-button.tsx +36 -0
  216. package/src/internal/plate-editor/plate-ui/media-audio-node-static.tsx +21 -0
  217. package/src/internal/plate-editor/plate-ui/media-audio-node.tsx +32 -0
  218. package/src/internal/plate-editor/plate-ui/media-embed-node.tsx +103 -0
  219. package/src/internal/plate-editor/plate-ui/media-file-node-static.tsx +30 -0
  220. package/src/internal/plate-editor/plate-ui/media-file-node.tsx +52 -0
  221. package/src/internal/plate-editor/plate-ui/media-image-node-static.tsx +37 -0
  222. package/src/internal/plate-editor/plate-ui/media-image-node.tsx +183 -0
  223. package/src/internal/plate-editor/plate-ui/media-placeholder-node.tsx +441 -0
  224. package/src/internal/plate-editor/plate-ui/media-preview-dialog.tsx +127 -0
  225. package/src/internal/plate-editor/plate-ui/media-toolbar-button.tsx +229 -0
  226. package/src/internal/plate-editor/plate-ui/media-toolbar.tsx +216 -0
  227. package/src/internal/plate-editor/plate-ui/media-upload-toast.tsx +73 -0
  228. package/src/internal/plate-editor/plate-ui/media-video-node-static.tsx +35 -0
  229. package/src/internal/plate-editor/plate-ui/media-video-node.tsx +119 -0
  230. package/src/internal/plate-editor/plate-ui/mention-node-static.tsx +46 -0
  231. package/src/internal/plate-editor/plate-ui/mention-node.tsx +79 -0
  232. package/src/internal/plate-editor/plate-ui/menu.tsx +532 -0
  233. package/src/internal/plate-editor/plate-ui/mode-toolbar-button.tsx +126 -0
  234. package/src/internal/plate-editor/plate-ui/more-toolbar-button.tsx +34 -0
  235. package/src/internal/plate-editor/plate-ui/paragraph-node-static.tsx +15 -0
  236. package/src/internal/plate-editor/plate-ui/paragraph-node.tsx +16 -0
  237. package/src/internal/plate-editor/plate-ui/popover.tsx +77 -0
  238. package/src/internal/plate-editor/plate-ui/progress.tsx +1 -0
  239. package/src/internal/plate-editor/plate-ui/remote-cursor-overlay.tsx +81 -0
  240. package/src/internal/plate-editor/plate-ui/resize-handle.tsx +88 -0
  241. package/src/internal/plate-editor/plate-ui/separator.tsx +43 -0
  242. package/src/internal/plate-editor/plate-ui/slash-node.tsx +435 -0
  243. package/src/internal/plate-editor/plate-ui/spinner.tsx +1 -0
  244. package/src/internal/plate-editor/plate-ui/suggestion-node-static.tsx +35 -0
  245. package/src/internal/plate-editor/plate-ui/suggestion-node.tsx +168 -0
  246. package/src/internal/plate-editor/plate-ui/suggestion-toolbar-button.tsx +24 -0
  247. package/src/internal/plate-editor/plate-ui/table-node-static.tsx +85 -0
  248. package/src/internal/plate-editor/plate-ui/table-node.tsx +285 -0
  249. package/src/internal/plate-editor/plate-ui/table-toolbar-button.tsx +254 -0
  250. package/src/internal/plate-editor/plate-ui/tabs.tsx +3 -0
  251. package/src/internal/plate-editor/plate-ui/textarea.tsx +58 -0
  252. package/src/internal/plate-editor/plate-ui/toc-node-static.tsx +142 -0
  253. package/src/internal/plate-editor/plate-ui/toc-node.tsx +57 -0
  254. package/src/internal/plate-editor/plate-ui/toc-sidebar.tsx +50 -0
  255. package/src/internal/plate-editor/plate-ui/toggle-node-static.tsx +18 -0
  256. package/src/internal/plate-editor/plate-ui/toggle-node.tsx +33 -0
  257. package/src/internal/plate-editor/plate-ui/toggle-toolbar-button.tsx +26 -0
  258. package/src/internal/plate-editor/plate-ui/toggle.tsx +3 -0
  259. package/src/internal/plate-editor/plate-ui/toolbar.tsx +380 -0
  260. package/src/internal/plate-editor/plate-ui/tooltip.tsx +149 -0
  261. package/src/internal/plate-editor/plate-ui/turn-into-toolbar-button.tsx +177 -0
  262. package/src/internal/plate-editor/types/index.ts +22 -0
  263. package/src/internal/plate-editor/vite.ts +284 -0
  264. package/src/internal/sheets/components/univer-sheets.tsx +1104 -0
  265. package/src/internal/sheets/i18n/context.tsx +183 -0
  266. package/src/internal/sheets/i18n/index.ts +19 -0
  267. package/src/internal/sheets/i18n/locales/de.json +21 -0
  268. package/src/internal/sheets/i18n/locales/el.json +21 -0
  269. package/src/internal/sheets/i18n/locales/en.json +21 -0
  270. package/src/internal/sheets/i18n/locales/es.json +21 -0
  271. package/src/internal/sheets/i18n/locales/fr.json +21 -0
  272. package/src/internal/sheets/i18n/locales/it.json +21 -0
  273. package/src/internal/sheets/i18n/locales/pt.json +21 -0
  274. package/src/internal/sheets/i18n/locales/sl.json +21 -0
  275. package/src/internal/sheets/i18n/locales/tr.json +21 -0
  276. package/src/internal/sheets/i18n/types.ts +23 -0
  277. package/src/internal/sheets/i18n/use-translation.ts +17 -0
  278. package/src/internal/sheets/index.ts +14 -0
  279. package/src/internal/sheets/types/css.d.ts +11 -0
  280. package/src/internal/sheets/types/index.ts +260 -0
  281. package/src/internal/sheets/xlsx.ts +1169 -0
  282. package/src/lib/api-client.ts +77 -0
  283. package/src/lib/assistant-api-actions.ts +549 -0
  284. package/src/lib/assistant-config.tsx +71 -0
  285. package/src/lib/class-utils.ts +40 -0
  286. package/src/lib/index.ts +7 -0
  287. package/src/lib/message-utils.ts +131 -0
  288. package/src/tools/tools-schema.json +512 -0
  289. package/src/types/index.ts +235 -0
  290. package/src/views/assistant-view.tsx +1137 -0
  291. package/src/views/canvas-app.tsx +839 -0
  292. package/src/views/canvas-code.tsx +93 -0
  293. package/src/views/canvas-deep-research.tsx +44 -0
  294. package/src/views/canvas-image.tsx +25 -0
  295. package/src/views/canvas-record-view.tsx +285 -0
  296. package/src/views/canvas-spreadsheet.tsx +125 -0
  297. package/src/views/canvas-text.tsx +52 -0
  298. package/src/views/canvas.tsx +274 -0
  299. package/src/views/chat-panel.tsx +149 -0
  300. package/src/views/index.ts +20 -0
  301. package/src/views/memories-panel.tsx +365 -0
  302. package/src/views/message-list.tsx +370 -0
  303. package/src/views/project-detail.tsx +257 -0
  304. package/src/views/projects-panel.tsx +131 -0
  305. package/src/views/sessions-list.tsx +98 -0
  306. package/src/views/sidebar-content.tsx +256 -0
  307. package/src/views/work-detail.tsx +98 -0
  308. package/src/vite.ts +284 -0
@@ -0,0 +1,1129 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Fragment, useCallback, useEffect, useReducer, useRef, useState, type ComponentProps, type RefObject
5
+ } from 'react';
6
+
7
+ import { getDraftCommentKey } from '@platejs/comment';
8
+ import { BlockSelectionPlugin } from '@platejs/selection/react';
9
+ import {
10
+ acceptSuggestion,
11
+ getSuggestionKey,
12
+ getTransientSuggestionKey,
13
+ keyId2SuggestionId,
14
+ rejectSuggestion
15
+ } from '@platejs/suggestion';
16
+ import { CheckIcon, PencilLineIcon, XIcon } from 'lucide-react';
17
+ import {
18
+ ElementApi,
19
+ KEYS,
20
+ type Node,
21
+ NodeApi,
22
+ type NodeEntry,
23
+ PathApi,
24
+ type SlateEditor,
25
+ type TCommentText,
26
+ type TElement,
27
+ TextApi,
28
+ type TSuggestionText
29
+ } from 'platejs';
30
+ import {
31
+ type PlateEditor,
32
+ useEditorContainerRef,
33
+ useEditorMounted,
34
+ useEditorPlugin,
35
+ useEditorRef,
36
+ useEditorSelector,
37
+ useEditorVersion,
38
+ usePluginOption,
39
+ usePluginOptions
40
+ } from 'platejs/react';
41
+
42
+ import { cn } from '@docyrus/ui-pro-shared/lib/utils';
43
+
44
+ import { Avatar, AvatarFallback, AvatarImage } from '@docyrus/ui-pro-shared/avatar';
45
+
46
+ import { Button } from './button';
47
+
48
+ import {
49
+ BLOCK_SUGGESTION,
50
+ type ResolvedSuggestion,
51
+ TYPE_TEXT_MAP
52
+ } from './block-suggestion';
53
+
54
+ import { Comment, CommentCreateForm, formatCommentDate } from './comment';
55
+
56
+ import { commentPlugin } from '../editor/plugins/comment-kit';
57
+ import {
58
+ discussionPlugin,
59
+ type TDiscussion
60
+ } from '../editor/plugins/discussion-kit';
61
+ import { suggestionPlugin } from '../editor/plugins/suggestion-kit';
62
+
63
+ // Inline useDebouncedCallback - simplified version
64
+ function useDebouncedCallback<T extends (...args: any[]) => any>(
65
+ func: T,
66
+ wait: number
67
+ ) {
68
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
69
+ const funcRef = useRef(func);
70
+
71
+ funcRef.current = func;
72
+
73
+ const debounced = useCallback(
74
+ (...args: Parameters<T>) => {
75
+ if (timeoutRef.current) {
76
+ clearTimeout(timeoutRef.current);
77
+ }
78
+ timeoutRef.current = setTimeout(() => {
79
+ funcRef.current(...args);
80
+ }, wait);
81
+ },
82
+ [wait]
83
+ );
84
+
85
+ useEffect(() => {
86
+ return () => {
87
+ if (timeoutRef.current) {
88
+ clearTimeout(timeoutRef.current);
89
+ }
90
+ };
91
+ }, []);
92
+
93
+ return debounced;
94
+ }
95
+
96
+ export function FloatingDiscussion() {
97
+ const mounted = useEditorMounted();
98
+ const isOverlapWithEditor = usePluginOption(
99
+ commentPlugin,
100
+ 'isOverlapWithEditor'
101
+ );
102
+
103
+ if (!mounted || isOverlapWithEditor) return null;
104
+
105
+ return <FloatingDiscussionContent />;
106
+ }
107
+
108
+ const getCommentTop = (
109
+ editor: SlateEditor,
110
+ {
111
+ node,
112
+ relativeElement,
113
+ topOffset = 30
114
+ }: {
115
+ node: TCommentText | TElement | TSuggestionText;
116
+ relativeElement: HTMLDivElement;
117
+ topOffset?: number;
118
+ }
119
+ ) => {
120
+ const commentLeafDomNode = editor.api.toDOMNode(node);
121
+
122
+ if (!commentLeafDomNode) return 0;
123
+
124
+ const relativeElementRect = relativeElement.getBoundingClientRect();
125
+ const { scrollTop } = relativeElement;
126
+ const commentLeafRect = commentLeafDomNode.getBoundingClientRect();
127
+
128
+ const top = commentLeafRect.top - relativeElementRect.top + scrollTop;
129
+
130
+ return top > topOffset ? top - topOffset : 0;
131
+ };
132
+
133
+ const updateActiveBelow = (
134
+ topMap: Record<string, number>,
135
+ domMap: Record<string, HTMLDivElement | null>,
136
+ activeId: string
137
+ ) => {
138
+ const discussionArray = Object.entries(topMap)
139
+ .map(([id, top]) => ({ id, top }))
140
+ .sort((a, b) => a.top - b.top);
141
+
142
+ const activeIndex = discussionArray.findIndex(({ id }) => id === activeId);
143
+
144
+ if (activeIndex === -1 || activeIndex === discussionArray.length - 1)
145
+ return topMap;
146
+
147
+ const activeElement = discussionArray[activeIndex];
148
+ const start = activeElement.top;
149
+ const end = start + (domMap[activeId]?.clientHeight ?? 100);
150
+
151
+ const nextElement = discussionArray[activeIndex + 1];
152
+ const nextStart = nextElement.top;
153
+
154
+ // Check if next element overlaps with active element
155
+ if (nextStart <= end) {
156
+ // Move all following elements down
157
+ const offset = end - nextStart + 10; // Add 10px gap
158
+
159
+ for (let i = activeIndex + 1; i < discussionArray.length; i++) {
160
+ discussionArray[i].top += offset;
161
+ }
162
+
163
+ return Object.fromEntries(discussionArray.map(d => [d.id, d.top]));
164
+ }
165
+
166
+ return topMap;
167
+ };
168
+
169
+ const updateActiveTop = (
170
+ topMap: Record<string, number>,
171
+ domMap: Record<string, HTMLDivElement | null>,
172
+ activeId: string,
173
+ targetTop: number
174
+ ) => {
175
+ const discussionArray = Object.entries(topMap)
176
+ .map(([id, top]) => ({ id, top }))
177
+ .sort((a, b) => a.top - b.top);
178
+
179
+ const index = discussionArray.findIndex(({ id }) => id === activeId);
180
+
181
+ if (index === -1) return topMap;
182
+
183
+ const currentTop = discussionArray[index].top;
184
+ const diff = targetTop - currentTop;
185
+
186
+ // Set position of active element
187
+ discussionArray[index].top = targetTop;
188
+
189
+ if (diff < 0) {
190
+ // Moving up - check for overlaps with previous elements
191
+ for (let i = index - 1; i >= 0; i--) {
192
+ const currentElement = discussionArray[i];
193
+ const nextElement = discussionArray[i + 1];
194
+ const elementHeight = domMap[currentElement.id]?.clientHeight ?? 100;
195
+
196
+ // Check if current element overlaps with next element
197
+ if (currentElement.top + elementHeight + 10 > nextElement.top) {
198
+ // Move current element up to avoid overlap
199
+ currentElement.top = nextElement.top - elementHeight - 10;
200
+ } else {
201
+ break; // No more overlaps
202
+ }
203
+ }
204
+ } else {
205
+ // Moving down - check for overlaps with next elements
206
+ const activeHeight = domMap[activeId]?.clientHeight ?? 100;
207
+ let activeBottom = targetTop + activeHeight + 10;
208
+
209
+ // Only move elements that would overlap with active element
210
+ for (let i = index + 1; i < discussionArray.length; i++) {
211
+ if (discussionArray[i].top < activeBottom) {
212
+ discussionArray[i].top = activeBottom;
213
+ activeBottom
214
+ = discussionArray[i].top
215
+ + (domMap[discussionArray[i].id]?.clientHeight ?? 100)
216
+ + 10;
217
+ } else {
218
+ break; // No more overlaps
219
+ }
220
+ }
221
+ }
222
+
223
+ return Object.fromEntries(discussionArray.map(d => [d.id, d.top]));
224
+ };
225
+
226
+ const updateTopCommenting = (
227
+ topMap: Record<string, number>,
228
+ domMap: Record<string, HTMLDivElement | null>
229
+ ) => {
230
+ const discussionArray = Object.entries(topMap)
231
+ .map(([id, topDistance]) => ({ id, topDistance }))
232
+ .sort((a, b) => a.topDistance - b.topDistance);
233
+
234
+ const index = discussionArray.findIndex(
235
+ ({ id }) => id === getDraftCommentKey()
236
+ );
237
+
238
+ if (index === -1) return topMap;
239
+
240
+ const targetTopDistance = discussionArray[index].topDistance;
241
+
242
+ // Find if any elements need to move up or down
243
+ let moveDistance = 0;
244
+
245
+ for (let i = 0; i < discussionArray.length; i++) {
246
+ const current = discussionArray[i];
247
+ const currentHeight = domMap[current.id]?.clientHeight ?? 100;
248
+
249
+ if (i < index) {
250
+ // Check if element needs to move up
251
+ const minRequiredSpace = targetTopDistance - (currentHeight + 10);
252
+
253
+ if (current.topDistance > minRequiredSpace) {
254
+ const distance = current.topDistance - minRequiredSpace;
255
+
256
+ moveDistance = Math.max(moveDistance, distance);
257
+ }
258
+ }
259
+ }
260
+
261
+ // Move elements up if needed
262
+ if (moveDistance > 0) {
263
+ for (let i = 0; i < index; i++) {
264
+ discussionArray[i].topDistance -= moveDistance;
265
+ }
266
+ }
267
+
268
+ // Check if next element overlaps with current element
269
+ const currentHeight = domMap[discussionArray[index].id]?.clientHeight ?? 100;
270
+ const currentBottom = targetTopDistance + currentHeight + 10;
271
+
272
+ if (index + 1 < discussionArray.length) {
273
+ const nextElement = discussionArray[index + 1];
274
+
275
+ if (nextElement.topDistance < currentBottom) {
276
+ // Only move elements that overlap
277
+ let currentTop = currentBottom;
278
+
279
+ for (let i = index + 1; i < discussionArray.length; i++) {
280
+ const element = discussionArray[i];
281
+
282
+ if (element.topDistance < currentTop) {
283
+ element.topDistance = currentTop;
284
+ const elementHeight = domMap[element.id]?.clientHeight ?? 100;
285
+
286
+ currentTop += elementHeight + 10;
287
+ } else {
288
+ break; // No more overlaps
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ return Object.fromEntries(discussionArray.map(d => [d.id, d.topDistance]));
295
+ };
296
+
297
+ const resolveOverlappingTop = (
298
+ topMap: Record<string, number>,
299
+ domMap: Record<string, HTMLDivElement | null>
300
+ ) => {
301
+ const discussionArray = Object.entries(topMap)
302
+ .map(([id, topDistance]) => ({ id, topDistance }))
303
+ .sort((a, b) => a.topDistance - b.topDistance);
304
+
305
+ // Iterate through each discussion from top to bottom, checking for overlap with previous discussions
306
+ for (let i = 1; i < discussionArray.length; i++) {
307
+ const currentDiscussion = discussionArray[i];
308
+ const currentElement = domMap[currentDiscussion.id];
309
+
310
+ if (!currentElement) continue;
311
+
312
+ // Calculate the range of current discussion
313
+ const currentStart = currentDiscussion.topDistance;
314
+ const currentEnd = currentStart + currentElement.clientHeight;
315
+
316
+ // Check for overlap with all previous discussions
317
+ for (let j = 0; j < i; j++) {
318
+ const previousDiscussion = discussionArray[j];
319
+ const previousElement = domMap[previousDiscussion.id];
320
+
321
+ if (!previousElement) continue;
322
+
323
+ const previousStart = previousDiscussion.topDistance;
324
+ const previousEnd = previousStart + previousElement.clientHeight;
325
+
326
+ // Check for overlap: condition for two intervals overlapping
327
+ if (
328
+ (currentStart <= previousEnd && currentEnd >= previousStart)
329
+ || (previousStart <= currentEnd && previousEnd >= currentStart)
330
+ ) {
331
+ // If overlapping, move current discussion below the previous one
332
+ currentDiscussion.topDistance = previousEnd + 10;
333
+ // Update current discussion range and check for overlaps again
334
+ i--;
335
+
336
+ break;
337
+ }
338
+ }
339
+ }
340
+
341
+ return Object.fromEntries(discussionArray.map(d => [d.id, d.topDistance]));
342
+ };
343
+
344
+ const useCommentingNode = () => useEditorSelector((editor) => {
345
+ if (!editor?.api || !editor.selection || editor.api.isExpanded()) return;
346
+
347
+ return editor.api.node<TCommentText>({
348
+ match: n => TextApi.isText(n) && n[KEYS.comment] && n[getDraftCommentKey()]
349
+ })?.[0];
350
+ }, []);
351
+
352
+ function FloatingDiscussionContent() {
353
+ const editorContainerRef = useEditorContainerRef();
354
+ const editor = useEditorRef();
355
+ const commentApi = editor.getApi(commentPlugin);
356
+ const suggestionApi = editor.getApi(suggestionPlugin);
357
+
358
+ const activeCommentId = usePluginOption(commentPlugin, 'activeId');
359
+ const activeSuggestionId = usePluginOption(suggestionPlugin, 'activeId');
360
+ const activeId = activeCommentId ?? activeSuggestionId;
361
+ const isOverlapWithEditor = usePluginOption(
362
+ commentPlugin,
363
+ 'isOverlapWithEditor'
364
+ );
365
+ const updateTimestamp = usePluginOption(commentPlugin, 'updateTimestamp');
366
+
367
+ const discussions = usePluginOption(discussionPlugin, 'discussions');
368
+ const domRef = useRef<Record<string, HTMLDivElement | null>>({});
369
+ const topRef = useRef<Record<string, number>>({});
370
+
371
+ const [, forceUpdate] = useReducer(x => x + 1, 0);
372
+
373
+ const commentingNode = useCommentingNode();
374
+ const version = useEditorVersion();
375
+
376
+ const suggestionEntriesMap = useRef<
377
+ Record<string, NodeEntry<TElement | TSuggestionText>[]>
378
+ >({});
379
+
380
+ useEffect(() => {
381
+ suggestionEntriesMap.current = {};
382
+
383
+ const allSuggestionNodes = suggestionApi.suggestion
384
+ .nodes({ at: [] })
385
+ .filter(([node]) => !node[getTransientSuggestionKey()]);
386
+
387
+ const suggestionIds = new Set(
388
+ allSuggestionNodes
389
+ .flatMap(([node]) => {
390
+ if (TextApi.isText(node)) {
391
+ const dataList = suggestionApi.suggestion.dataList(node);
392
+ const includeUpdate = dataList.some(
393
+ data => data.type === 'update'
394
+ );
395
+
396
+ if (!includeUpdate) return suggestionApi.suggestion.nodeId(node);
397
+
398
+ return dataList
399
+ .filter(data => data.type === 'update')
400
+ .map(d => d.id);
401
+ }
402
+ if (ElementApi.isElement(node)) {
403
+ return suggestionApi.suggestion.nodeId(node);
404
+ }
405
+
406
+ return null;
407
+ })
408
+ .filter(Boolean)
409
+ );
410
+
411
+ suggestionIds.forEach((id) => {
412
+ if (!id) return;
413
+
414
+ const entries = [
415
+ ...editor.api.nodes<TElement | TSuggestionText>({
416
+ at: [],
417
+ mode: 'all',
418
+ match: n => (n[KEYS.suggestion] && n[getSuggestionKey(id)])
419
+ || suggestionApi.suggestion.nodeId(n as TElement) === id
420
+ })
421
+ ];
422
+
423
+ suggestionEntriesMap.current[id] = entries;
424
+ });
425
+ }, [editor, suggestionApi.suggestion, version]);
426
+
427
+ const suggestionList = Object.entries(suggestionEntriesMap.current).map(
428
+ ([id, entries]) => ({
429
+ id,
430
+ entries
431
+ })
432
+ );
433
+
434
+ const renderFloatingDiscussion = useCallback(() => {
435
+ if (isOverlapWithEditor) return;
436
+
437
+ topRef.current = {};
438
+
439
+ discussions.forEach((discussion) => {
440
+ if (
441
+ discussion.isResolved
442
+ || !commentApi.comment.has({ id: discussion.id })
443
+ )
444
+ return;
445
+
446
+ const commentLeafEntry = commentApi.comment.node({
447
+ id: discussion.id,
448
+ at: []
449
+ });
450
+
451
+ if (!commentLeafEntry) return;
452
+
453
+ const commentLeaf = commentLeafEntry[0];
454
+
455
+ const topDistance = getCommentTop(editor, {
456
+ node: commentLeaf,
457
+ relativeElement: editorContainerRef.current!
458
+ });
459
+
460
+ topRef.current[discussion.id] = topDistance;
461
+ });
462
+
463
+ suggestionList.forEach(({ id, entries }) => {
464
+ if (!id) return;
465
+
466
+ const topDistance = getCommentTop(editor, {
467
+ node: entries[0][0],
468
+ relativeElement: editorContainerRef.current!
469
+ });
470
+
471
+ topRef.current[id] = topDistance;
472
+ });
473
+
474
+ topRef.current = resolveOverlappingTop(topRef.current, domRef.current);
475
+ forceUpdate();
476
+
477
+ // eslint-disable-next-line react-hooks/exhaustive-deps
478
+ }, [
479
+ discussions.length,
480
+ suggestionList.length,
481
+ editorContainerRef,
482
+ isOverlapWithEditor
483
+ ]);
484
+
485
+ const renderFloatingCreateForm = useCallback(() => {
486
+ if (!commentingNode || activeId !== getDraftCommentKey()) return;
487
+
488
+ const topDistance = getCommentTop(editor, {
489
+ node: commentingNode,
490
+ relativeElement: editorContainerRef.current!
491
+ });
492
+
493
+ topRef.current[getDraftCommentKey()] = topDistance;
494
+
495
+ topRef.current = updateTopCommenting(topRef.current, domRef.current);
496
+
497
+ forceUpdate();
498
+ }, [
499
+ activeId,
500
+ commentingNode,
501
+ editor,
502
+ editorContainerRef
503
+ ]);
504
+
505
+ const debouncedUpdateFloat = useDebouncedCallback(
506
+ renderFloatingDiscussion,
507
+ 500
508
+ );
509
+
510
+ useEffect(() => {
511
+ if (updateTimestamp) {
512
+ debouncedUpdateFloat();
513
+ }
514
+ }, [debouncedUpdateFloat, updateTimestamp]);
515
+
516
+ useEffect(() => {
517
+ if (!discussions) return;
518
+
519
+ setTimeout(() => {
520
+ renderFloatingDiscussion();
521
+ }, 0);
522
+ }, [discussions, renderFloatingDiscussion]);
523
+
524
+ useEffect(() => {
525
+ if (!activeId || !domRef.current[activeId]) return;
526
+
527
+ const resizeObserver = new ResizeObserver(() => {
528
+ topRef.current = updateActiveBelow(
529
+ topRef.current,
530
+ domRef.current,
531
+ activeId
532
+ );
533
+
534
+ forceUpdate();
535
+ });
536
+
537
+ resizeObserver.observe(domRef.current[activeId]);
538
+
539
+ return () => {
540
+ resizeObserver.disconnect();
541
+ };
542
+ }, [activeId]);
543
+
544
+ useEffect(() => {
545
+ if (!activeId) {
546
+ return renderFloatingDiscussion();
547
+ }
548
+
549
+ const activeNode = commentApi.comment.node({ id: activeId, at: [] });
550
+ const activeSuggestionNode = suggestionApi.suggestion.node({
551
+ id: activeId,
552
+ at: [],
553
+ isText: true
554
+ });
555
+
556
+ if (!activeNode && !activeSuggestionNode) return;
557
+
558
+ topRef.current = updateActiveTop(
559
+ topRef.current,
560
+ domRef.current,
561
+ activeId,
562
+ getCommentTop(editor, {
563
+ node: activeNode?.[0] || activeSuggestionNode![0],
564
+ relativeElement: editorContainerRef.current!
565
+ })
566
+ );
567
+
568
+ forceUpdate();
569
+ }, [
570
+ activeId,
571
+ editor,
572
+ commentingNode,
573
+ editorContainerRef,
574
+ renderFloatingDiscussion,
575
+ commentApi.comment,
576
+ suggestionApi.suggestion
577
+ ]);
578
+
579
+ useEffect(() => {
580
+ if (!commentingNode || !domRef.current[getDraftCommentKey()]) {
581
+ return;
582
+ }
583
+
584
+ const resizeObserver = new ResizeObserver(() => {
585
+ renderFloatingCreateForm();
586
+ });
587
+
588
+ resizeObserver.observe(domRef.current[getDraftCommentKey()]!);
589
+
590
+ return () => {
591
+ resizeObserver.disconnect();
592
+ };
593
+ }, [
594
+ commentingNode,
595
+ editor,
596
+ editorContainerRef,
597
+ renderFloatingCreateForm,
598
+ renderFloatingDiscussion
599
+ ]);
600
+
601
+ useEffect(() => {
602
+ if (!isOverlapWithEditor) {
603
+ renderFloatingDiscussion();
604
+ }
605
+ }, [isOverlapWithEditor, renderFloatingDiscussion]);
606
+
607
+ // Only show if there are discussions/suggestions or active commenting
608
+ const hasUnresolvedDiscussions = discussions.some((d: TDiscussion) => !d.isResolved);
609
+ const hasSuggestions = suggestionList.length > 0;
610
+
611
+ if (!commentingNode && !hasUnresolvedDiscussions && !hasSuggestions && !activeId) {
612
+ return null;
613
+ }
614
+
615
+ return (
616
+ <>
617
+ {/* Unsubmit comment */}
618
+ {commentingNode && (
619
+ <div
620
+ className="absolute right-[80px] w-[288px] cursor-pointer rounded-lg border bg-popover p-3 transition-transform duration-200"
621
+ ref={(el) => {
622
+ domRef.current[getDraftCommentKey()] = el;
623
+ }}
624
+ style={{
625
+ top: topRef.current[getDraftCommentKey()] ?? -9999
626
+ }}>
627
+ <CommentCreateForm focusOnMount />
628
+ </div>
629
+ )}
630
+
631
+ {discussions.map(
632
+ discussion => !discussion.isResolved
633
+ && commentApi.comment.has({ id: discussion.id }) && (
634
+ <FloatingCommentsContent
635
+ discussion={discussion}
636
+ domRef={domRef}
637
+ key={discussion.id}
638
+ ref={(el) => {
639
+ domRef.current[discussion.id] = el;
640
+ }}
641
+ top={topRef.current[discussion.id] ?? 0} />
642
+ )
643
+ )}
644
+
645
+ {suggestionList.map(
646
+ ({ id, entries }) => id && (
647
+ <FloatingSuggestionContent
648
+ entries={entries}
649
+ id={id}
650
+ key={id}
651
+ ref={(el) => {
652
+ domRef.current[id] = el;
653
+ }}
654
+ top={topRef.current[id] ?? 0} />
655
+ )
656
+ )}
657
+ </>
658
+ );
659
+ }
660
+
661
+ type FloatingCommentsContentProps = {
662
+ discussion: TDiscussion;
663
+ domRef: RefObject<Record<string, HTMLDivElement | null>>;
664
+ top: number;
665
+ };
666
+
667
+ function FloatingCommentsContent({
668
+ discussion,
669
+ ref,
670
+ top
671
+ }: ComponentProps<'div'> & FloatingCommentsContentProps) {
672
+ const editor = useEditorRef();
673
+
674
+ const { activeId, hoverId } = usePluginOptions(
675
+ commentPlugin,
676
+ ({ activeId, hoverId }) => ({
677
+ activeId,
678
+ hoverId
679
+ })
680
+ );
681
+
682
+ const [editingId, setEditingId] = useState<string | null>(null);
683
+
684
+ const setHoverId = (id: string | null) => {
685
+ /*
686
+ * If dropdown menu open, do not unset the active state since it will make dropdown menu open in the wrong position
687
+ * Notion has the same issue
688
+ */
689
+ if (document.activeElement?.closest('[data-radix-menu-content]')) return;
690
+
691
+ editor.setOption(commentPlugin, 'hoverId', id);
692
+ };
693
+
694
+ const highlightDiscussion = (editor: PlateEditor, id: string) => {
695
+ editor.setOption(commentPlugin, 'activeId', id);
696
+ editor.setOption(suggestionPlugin, 'activeId', null);
697
+ const leaf = editor.api.node({
698
+ at: [],
699
+ match: n => TextApi.isText(n)
700
+ && n[KEYS.comment]
701
+ && editor.getApi(commentPlugin).comment.nodeId(n) === id
702
+ });
703
+
704
+ if (!leaf) return;
705
+
706
+ const parent = NodeApi.get<Node>(editor, leaf[1].slice(0, 1));
707
+
708
+ editor
709
+ .getApi(BlockSelectionPlugin)
710
+ .blockSelection.addSelectedRow(parent!.id as string, {
711
+ clear: false,
712
+ delay: 1000
713
+ });
714
+ };
715
+
716
+ return (
717
+ <div
718
+ className={cn(
719
+ 'absolute right-20 z-10 w-72 animate-fade-in cursor-pointer rounded-lg border bg-popover p-3 transition-transform duration-200',
720
+ '[&[data-hover=true][data-active=false]]:-translate-x-2 [&[data-hover=true][data-active=false]]:bg-muted',
721
+ '[&[data-active=true]]:-translate-x-2'
722
+ )}
723
+ data-active={activeId === discussion.id}
724
+ data-discussion-id={discussion.id}
725
+ data-hover={hoverId === discussion.id}
726
+ onClick={() => highlightDiscussion(editor, discussion.id)}
727
+ onMouseEnter={() => setHoverId(discussion.id)}
728
+ onMouseLeave={() => setHoverId(null)}
729
+ ref={ref}
730
+ style={{
731
+ top
732
+ }}>
733
+ {discussion.comments.length >= 3 && activeId !== discussion.id ? (
734
+ <>
735
+ <Comment
736
+ comment={discussion.comments[0]}
737
+ discussionLength={discussion.comments.length}
738
+ documentContent={discussion?.documentContent}
739
+ editingId={editingId}
740
+ index={0}
741
+ key={discussion.comments[0].id}
742
+ onEditorClick={() => highlightDiscussion(editor, discussion.id)}
743
+ setEditingId={setEditingId} />
744
+ <div className="relative mb-1 ml-[26px] flex h-7 items-center rounded-md pl-1.5 text-muted-foreground text-sm hover:bg-muted">
745
+ <div className="absolute top-[-5px] left-[-14px] h-full w-0.5 shrink-0 bg-muted" />
746
+ <div className="ml-2">
747
+ Show {discussion.comments.length - 2} replies
748
+ </div>
749
+ </div>
750
+ <Comment
751
+ comment={discussion.comments.at(-1)!}
752
+ discussionLength={discussion.comments.length}
753
+ documentContent={discussion?.documentContent}
754
+ editingId={editingId}
755
+ index={discussion.comments.length - 1}
756
+ key={discussion.comments.at(-1)!.id}
757
+ onEditorClick={() => highlightDiscussion(editor, discussion.id)}
758
+ setEditingId={setEditingId} />
759
+ </>
760
+ ) : (
761
+ discussion.comments.map((comment, index) => (
762
+ <Comment
763
+ comment={comment}
764
+ discussionLength={discussion.comments.length}
765
+ documentContent={discussion?.documentContent}
766
+ editingId={editingId}
767
+ index={index}
768
+ key={comment.id ?? index}
769
+ onEditorClick={() => highlightDiscussion(editor, discussion.id)}
770
+ setEditingId={setEditingId} />
771
+ ))
772
+ )}
773
+
774
+ {activeId === discussion.id && (
775
+ <CommentCreateForm discussionId={discussion.id} />
776
+ )}
777
+ </div>
778
+ );
779
+ }
780
+
781
+ type FloatingSuggestionContentProps = {
782
+ id: string;
783
+ entries: NodeEntry<TElement | TSuggestionText>[];
784
+ top: number;
785
+ };
786
+
787
+ const FloatingSuggestionContent = ({
788
+ id,
789
+ entries,
790
+ ref,
791
+ top
792
+ }: ComponentProps<'div'> & FloatingSuggestionContentProps) => {
793
+ const { api, editor, setOption } = useEditorPlugin(suggestionPlugin);
794
+ const nodeData = api.suggestion.suggestionData(entries[0][0]);
795
+
796
+ const { activeId, hoverId } = usePluginOptions(
797
+ suggestionPlugin,
798
+ ({ activeId, hoverId }) => ({
799
+ activeId,
800
+ hoverId
801
+ })
802
+ );
803
+
804
+ const userId = usePluginOption(discussionPlugin, 'currentUserId');
805
+ const userData = usePluginOption(discussionPlugin, 'user', userId);
806
+ const discussions = usePluginOption(discussionPlugin, 'discussions');
807
+
808
+ const [editingId, setEditingId] = useState<string | null>(null);
809
+
810
+ if (entries.length === 0) return null;
811
+
812
+ // move line break to the end
813
+ entries.sort(([, path1], [, path2]) => PathApi.isChild(path1, path2) ? -1 : 1);
814
+
815
+ let newText = '';
816
+ let text = '';
817
+ let properties: any = {};
818
+ let newProperties: any = {};
819
+
820
+ // overlapping suggestion
821
+ entries.forEach(([node]) => {
822
+ if (TextApi.isText(node)) {
823
+ const dataList = api.suggestion.dataList(node);
824
+
825
+ dataList.forEach((data) => {
826
+ if (data.id !== id) return;
827
+
828
+ switch (data.type) {
829
+ case 'insert': {
830
+ newText += node.text;
831
+
832
+ break;
833
+ }
834
+
835
+ case 'remove': {
836
+ text += node.text;
837
+
838
+ break;
839
+ }
840
+
841
+ case 'update': {
842
+ properties = {
843
+ ...properties,
844
+ ...data.properties
845
+ };
846
+ newProperties = {
847
+ ...newProperties,
848
+ ...data.newProperties
849
+ };
850
+ newText += node.text;
851
+
852
+ break;
853
+ }
854
+ }
855
+ });
856
+ } else {
857
+ const lineBreakData = api.suggestion.isBlockSuggestion(node) ? node.suggestion : undefined;
858
+
859
+ if (lineBreakData?.id !== keyId2SuggestionId(id)) return;
860
+ if (lineBreakData.type === 'insert') {
861
+ newText += lineBreakData.isLineBreak ? BLOCK_SUGGESTION : BLOCK_SUGGESTION + TYPE_TEXT_MAP[node.type](node);
862
+ } else if (lineBreakData.type === 'remove') {
863
+ text += lineBreakData.isLineBreak ? BLOCK_SUGGESTION : BLOCK_SUGGESTION + TYPE_TEXT_MAP[node.type](node);
864
+ }
865
+ }
866
+ });
867
+
868
+ if (!nodeData) return null;
869
+
870
+ const comments = discussions.find(d => d.id === id)?.comments || [];
871
+ const createdAt = new Date(nodeData.createdAt);
872
+ const keyId = getSuggestionKey(id);
873
+
874
+ const suggestionText2Array = (text: string) => {
875
+ if (text === BLOCK_SUGGESTION) return ['line breaks'];
876
+
877
+ return text.split(BLOCK_SUGGESTION).filter(Boolean);
878
+ };
879
+
880
+ const accept = (suggestion: ResolvedSuggestion) => {
881
+ api.suggestion.withoutSuggestions(() => {
882
+ acceptSuggestion(editor, suggestion);
883
+ });
884
+ };
885
+
886
+ const reject = (suggestion: ResolvedSuggestion) => {
887
+ api.suggestion.withoutSuggestions(() => {
888
+ rejectSuggestion(editor, suggestion);
889
+ });
890
+ };
891
+
892
+ let suggestion: ResolvedSuggestion;
893
+
894
+ if (nodeData.type === 'update') {
895
+ suggestion = {
896
+ comments,
897
+ createdAt,
898
+ keyId,
899
+ newProperties,
900
+ newText,
901
+ properties,
902
+ suggestionId: keyId2SuggestionId(id),
903
+ type: 'update',
904
+ userId: nodeData.userId
905
+ };
906
+ } else if (newText.length > 0 && text.length > 0) {
907
+ suggestion = {
908
+ comments,
909
+ createdAt,
910
+ keyId,
911
+ newText,
912
+ suggestionId: keyId2SuggestionId(id),
913
+ text,
914
+ type: 'replace',
915
+ userId: nodeData.userId
916
+ };
917
+ } else if (newText.length > 0) {
918
+ suggestion = {
919
+ comments,
920
+ createdAt,
921
+ keyId,
922
+ newText,
923
+ suggestionId: keyId2SuggestionId(id),
924
+ type: 'insert',
925
+ userId: nodeData.userId
926
+ };
927
+ } else if (text.length > 0) {
928
+ suggestion = {
929
+ comments,
930
+ createdAt,
931
+ keyId,
932
+ suggestionId: keyId2SuggestionId(id),
933
+ text,
934
+ type: 'remove',
935
+ userId: nodeData.userId
936
+ };
937
+ } else {
938
+ return null;
939
+ }
940
+
941
+ const highlightSuggestion = (editor: PlateEditor, id: string) => {
942
+ editor.setOption(suggestionPlugin, 'activeId', id);
943
+ editor.setOption(commentPlugin, 'activeId', null);
944
+
945
+ const leaf = editor.api.node({
946
+ at: [],
947
+ match: n => n[KEYS.suggestion]
948
+ && editor.getApi(suggestionPlugin).suggestion.nodeId(n as any) === id
949
+ });
950
+
951
+ if (!leaf) return;
952
+
953
+ const parent = NodeApi.get<Node>(editor, leaf[1].slice(0, 1));
954
+
955
+ editor
956
+ .getApi(BlockSelectionPlugin)
957
+ .blockSelection.addSelectedRow(parent!.id as string, {
958
+ clear: false,
959
+ delay: 1000
960
+ });
961
+ };
962
+
963
+ // Only show if this suggestion is active or hovered
964
+ if (activeId !== id && hoverId !== id) {
965
+ return null;
966
+ }
967
+
968
+ return (
969
+ <div
970
+ className={cn(
971
+ 'absolute right-20 z-10 w-72 animate-fade-in cursor-pointer rounded-lg border bg-popover p-3 transition-transform duration-200',
972
+ '[&[data-hover=true][data-active=false]]:-translate-x-2 [&[data-hover=true][data-active=false]]:bg-muted',
973
+ '[&[data-active=true]]:-translate-x-2'
974
+ )}
975
+ data-active={activeId === id}
976
+ data-discussion-id={id}
977
+ data-hover={hoverId === id}
978
+ onClick={() => highlightSuggestion(editor, id)}
979
+ onMouseEnter={() => setOption('hoverId', id)}
980
+ onMouseLeave={() => setOption('hoverId', null)}
981
+ ref={ref}
982
+ style={{ top }}>
983
+ <div className="flex flex-col">
984
+ <div className="relative flex items-center">
985
+ {userData && (
986
+ <>
987
+ <Avatar className="relative mr-2 size-6">
988
+ <AvatarImage alt={userData.name} src={userData.avatarUrl} />
989
+ <AvatarFallback>{userData.name?.[0]}</AvatarFallback>
990
+ </Avatar>
991
+ <PencilLineIcon className="-bottom-2 absolute left-4 size-4 rounded-[50%] bg-brand-foreground p-0.5 text-brand/80" />
992
+ </>
993
+ )}
994
+ <h4 className="font-semibold text-sm leading-none">
995
+ {userData?.name}
996
+ </h4>
997
+ <div className="ml-1.5 text-muted-foreground/80 text-xs leading-none">
998
+ <span className="mr-1">
999
+ {formatCommentDate(suggestion.createdAt)}
1000
+ </span>
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <div className="relative mt-1 mb-4 pl-[32px]">
1005
+ <div className="flex flex-col gap-2">
1006
+ {suggestion.type === 'remove'
1007
+ && suggestionText2Array(suggestion.text!).map((text, index) => (
1008
+ <div className="flex items-center gap-2" key={index}>
1009
+ <span className="text-muted-foreground text-sm">Delete:</span>
1010
+ <span className="text-sm">{text}</span>
1011
+ </div>
1012
+ ))}
1013
+
1014
+ {suggestion.type === 'insert'
1015
+ && suggestionText2Array(suggestion.newText!).map((text, index) => (
1016
+ <div className="flex items-center gap-2" key={index}>
1017
+ <span className="text-muted-foreground text-sm">Add:</span>
1018
+ <span className="text-sm">"{text || 'line breaks'}"</span>
1019
+ </div>
1020
+ ))}
1021
+
1022
+ {suggestion.type === 'replace' && (
1023
+ <div className="flex flex-col gap-2">
1024
+ {suggestionText2Array(suggestion.newText!).map(
1025
+ (text, index) => (
1026
+ <Fragment key={index}>
1027
+ <div className="flex items-center text-brand/80">
1028
+ <span className="text-sm">With:</span>
1029
+ <span className="text-sm">
1030
+ "{text || 'line breaks'}"
1031
+ </span>
1032
+ </div>
1033
+ </Fragment>
1034
+ )
1035
+ )}
1036
+
1037
+ {suggestionText2Array(suggestion.text!).map((text, index) => (
1038
+ <Fragment key={index}>
1039
+ <div className="flex items-center">
1040
+ <span className="text-muted-foreground text-sm">
1041
+ {index === 0 ? 'Replace:' : 'Delete:'}
1042
+ </span>
1043
+ <span className="text-sm">"{text || 'line breaks'}"</span>
1044
+ </div>
1045
+ </Fragment>
1046
+ ))}
1047
+ </div>
1048
+ )}
1049
+
1050
+ {suggestion.type === 'update' && (
1051
+ <div className="flex items-center gap-2">
1052
+ <span className="text-muted-foreground text-sm">
1053
+ {Object.keys(suggestion.properties).map(key => (
1054
+ <span key={key}>Un{key}</span>
1055
+ ))}
1056
+ {Object.keys(suggestion.newProperties).map(key => (
1057
+ <span key={key}>
1058
+ {key.charAt(0).toUpperCase() + key.slice(1)}
1059
+ </span>
1060
+ ))}
1061
+ </span>
1062
+ <span className="text-sm">"{suggestion.newText}"</span>
1063
+ </div>
1064
+ )}
1065
+ </div>
1066
+ </div>
1067
+
1068
+ {suggestion.comments.length >= 3 && activeId !== id ? (
1069
+ <>
1070
+ <Comment
1071
+ comment={suggestion.comments[0]}
1072
+ discussionLength={suggestion.comments.length}
1073
+ documentContent="__suggestion__"
1074
+ editingId={editingId}
1075
+ index={0}
1076
+ key={suggestion.comments[0].id}
1077
+ setEditingId={setEditingId} />
1078
+ <div className="relative mb-1 ml-[26px] flex h-7 items-center rounded-md pl-1.5 text-muted-foreground text-sm hover:bg-muted">
1079
+ <div className="absolute top-[-5px] left-[-14px] h-full w-0.5 shrink-0 bg-muted" />
1080
+ <div className="ml-2">
1081
+ Show {suggestion.comments.length - 2} replies
1082
+ </div>
1083
+ </div>
1084
+ <Comment
1085
+ comment={suggestion.comments.at(-1)!}
1086
+ discussionLength={suggestion.comments.length}
1087
+ documentContent="__suggestion__"
1088
+ editingId={editingId}
1089
+ index={suggestion.comments.length - 1}
1090
+ key={suggestion.comments.at(-1)!.id}
1091
+ setEditingId={setEditingId} />
1092
+ </>
1093
+ ) : (
1094
+ suggestion.comments.map((comment, index) => (
1095
+ <Comment
1096
+ comment={comment}
1097
+ discussionLength={suggestion.comments.length}
1098
+ documentContent="__suggestion__"
1099
+ editingId={editingId}
1100
+ index={index}
1101
+ key={comment.id ?? index}
1102
+ setEditingId={setEditingId} />
1103
+ ))
1104
+ )}
1105
+
1106
+ {hoverId === id && (
1107
+ <div className="absolute top-4 right-4 flex gap-2">
1108
+ <Button
1109
+ className="h-6 p-1 text-muted-foreground"
1110
+ onClick={() => accept(suggestion)}
1111
+ variant="ghost">
1112
+ <CheckIcon className="size-4" />
1113
+ </Button>
1114
+ <Button
1115
+ className="h-6 p-1 text-muted-foreground"
1116
+ onClick={() => reject(suggestion)}
1117
+ variant="ghost">
1118
+ <XIcon className="size-4" />
1119
+ </Button>
1120
+ </div>
1121
+ )}
1122
+
1123
+ {activeId === id && (
1124
+ <CommentCreateForm discussionId={suggestion.suggestionId} />
1125
+ )}
1126
+ </div>
1127
+ </div>
1128
+ );
1129
+ };