@brainfish-ai/devdoc 0.1.21

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 (268) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +415 -0
  3. package/bin/devdoc.js +13 -0
  4. package/dist/cli/commands/build.d.ts +5 -0
  5. package/dist/cli/commands/build.js +87 -0
  6. package/dist/cli/commands/check.d.ts +1 -0
  7. package/dist/cli/commands/check.js +143 -0
  8. package/dist/cli/commands/create.d.ts +24 -0
  9. package/dist/cli/commands/create.js +387 -0
  10. package/dist/cli/commands/deploy.d.ts +9 -0
  11. package/dist/cli/commands/deploy.js +433 -0
  12. package/dist/cli/commands/dev.d.ts +6 -0
  13. package/dist/cli/commands/dev.js +139 -0
  14. package/dist/cli/commands/init.d.ts +11 -0
  15. package/dist/cli/commands/init.js +238 -0
  16. package/dist/cli/commands/keys.d.ts +12 -0
  17. package/dist/cli/commands/keys.js +165 -0
  18. package/dist/cli/commands/start.d.ts +5 -0
  19. package/dist/cli/commands/start.js +56 -0
  20. package/dist/cli/commands/upload.d.ts +13 -0
  21. package/dist/cli/commands/upload.js +238 -0
  22. package/dist/cli/commands/whoami.d.ts +8 -0
  23. package/dist/cli/commands/whoami.js +91 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +106 -0
  26. package/dist/config/index.d.ts +80 -0
  27. package/dist/config/index.js +133 -0
  28. package/dist/constants.d.ts +9 -0
  29. package/dist/constants.js +13 -0
  30. package/dist/index.d.ts +7 -0
  31. package/dist/index.js +12 -0
  32. package/dist/utils/logger.d.ts +16 -0
  33. package/dist/utils/logger.js +61 -0
  34. package/dist/utils/paths.d.ts +16 -0
  35. package/dist/utils/paths.js +50 -0
  36. package/package.json +51 -0
  37. package/renderer/app/api/assets/[...path]/route.ts +123 -0
  38. package/renderer/app/api/assets/route.ts +124 -0
  39. package/renderer/app/api/assets/upload/route.ts +177 -0
  40. package/renderer/app/api/auth-schemes/route.ts +77 -0
  41. package/renderer/app/api/chat/route.ts +858 -0
  42. package/renderer/app/api/codegen/route.ts +72 -0
  43. package/renderer/app/api/collections/route.ts +1016 -0
  44. package/renderer/app/api/debug/route.ts +53 -0
  45. package/renderer/app/api/deploy/route.ts +234 -0
  46. package/renderer/app/api/device/route.ts +42 -0
  47. package/renderer/app/api/docs/route.ts +187 -0
  48. package/renderer/app/api/keys/regenerate/route.ts +80 -0
  49. package/renderer/app/api/openapi-spec/route.ts +151 -0
  50. package/renderer/app/api/projects/[slug]/route.ts +153 -0
  51. package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
  52. package/renderer/app/api/projects/register/route.ts +152 -0
  53. package/renderer/app/api/proxy/route.ts +149 -0
  54. package/renderer/app/api/proxy-stream/route.ts +168 -0
  55. package/renderer/app/api/redirects/route.ts +47 -0
  56. package/renderer/app/api/schema/route.ts +65 -0
  57. package/renderer/app/api/subdomains/check/route.ts +172 -0
  58. package/renderer/app/api/suggestions/route.ts +144 -0
  59. package/renderer/app/favicon.ico +0 -0
  60. package/renderer/app/globals.css +1103 -0
  61. package/renderer/app/layout.tsx +47 -0
  62. package/renderer/app/llms-full.txt/route.ts +346 -0
  63. package/renderer/app/llms.txt/route.ts +279 -0
  64. package/renderer/app/page.tsx +14 -0
  65. package/renderer/app/robots.txt/route.ts +84 -0
  66. package/renderer/app/sitemap.xml/route.ts +199 -0
  67. package/renderer/components/docs/index.ts +12 -0
  68. package/renderer/components/docs/mdx/accordion.tsx +169 -0
  69. package/renderer/components/docs/mdx/badge.tsx +132 -0
  70. package/renderer/components/docs/mdx/callouts.tsx +154 -0
  71. package/renderer/components/docs/mdx/cards.tsx +213 -0
  72. package/renderer/components/docs/mdx/changelog.tsx +120 -0
  73. package/renderer/components/docs/mdx/code-block.tsx +186 -0
  74. package/renderer/components/docs/mdx/code-group.tsx +421 -0
  75. package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
  76. package/renderer/components/docs/mdx/frame.tsx +112 -0
  77. package/renderer/components/docs/mdx/highlight.tsx +151 -0
  78. package/renderer/components/docs/mdx/iframe.tsx +134 -0
  79. package/renderer/components/docs/mdx/image.tsx +235 -0
  80. package/renderer/components/docs/mdx/index.ts +204 -0
  81. package/renderer/components/docs/mdx/mermaid.tsx +240 -0
  82. package/renderer/components/docs/mdx/param-field.tsx +200 -0
  83. package/renderer/components/docs/mdx/steps.tsx +113 -0
  84. package/renderer/components/docs/mdx/tabs.tsx +86 -0
  85. package/renderer/components/docs/mdx-renderer.tsx +100 -0
  86. package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
  87. package/renderer/components/docs/navigation/index.ts +8 -0
  88. package/renderer/components/docs/navigation/page-nav.tsx +64 -0
  89. package/renderer/components/docs/navigation/sidebar.tsx +515 -0
  90. package/renderer/components/docs/navigation/toc.tsx +113 -0
  91. package/renderer/components/docs/notice.tsx +105 -0
  92. package/renderer/components/docs-header.tsx +274 -0
  93. package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
  94. package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
  95. package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
  96. package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
  97. package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
  98. package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
  99. package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
  100. package/renderer/components/docs-viewer/agent/index.tsx +6 -0
  101. package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
  102. package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
  103. package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
  104. package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
  105. package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
  106. package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
  107. package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
  108. package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
  109. package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
  110. package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
  111. package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
  112. package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
  113. package/renderer/components/docs-viewer/content/index.tsx +29 -0
  114. package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
  115. package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
  116. package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
  117. package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
  118. package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
  119. package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
  120. package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
  121. package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
  122. package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
  123. package/renderer/components/docs-viewer/index.tsx +1466 -0
  124. package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
  125. package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
  126. package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
  127. package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
  128. package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
  129. package/renderer/components/docs-viewer/playground/index.tsx +671 -0
  130. package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
  131. package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
  132. package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
  133. package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
  134. package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
  135. package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
  136. package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
  137. package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
  138. package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
  139. package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
  140. package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
  141. package/renderer/components/docs-viewer/search/index.ts +2 -0
  142. package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
  143. package/renderer/components/docs-viewer/search/use-search.ts +117 -0
  144. package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
  145. package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
  146. package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
  147. package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
  148. package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
  149. package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
  150. package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
  151. package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
  152. package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
  153. package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
  154. package/renderer/components/theme-provider.tsx +11 -0
  155. package/renderer/components/theme-toggle.tsx +76 -0
  156. package/renderer/components/ui/badge.tsx +46 -0
  157. package/renderer/components/ui/button.tsx +59 -0
  158. package/renderer/components/ui/dialog.tsx +118 -0
  159. package/renderer/components/ui/dropdown-menu.tsx +257 -0
  160. package/renderer/components/ui/input.tsx +21 -0
  161. package/renderer/components/ui/label.tsx +24 -0
  162. package/renderer/components/ui/navigation-menu.tsx +168 -0
  163. package/renderer/components/ui/select.tsx +190 -0
  164. package/renderer/components/ui/spinner.tsx +114 -0
  165. package/renderer/components/ui/tabs.tsx +66 -0
  166. package/renderer/components/ui/tooltip.tsx +61 -0
  167. package/renderer/hooks/use-code-copy.ts +88 -0
  168. package/renderer/hooks/use-openapi-title.ts +44 -0
  169. package/renderer/lib/api-docs/agent/index.ts +6 -0
  170. package/renderer/lib/api-docs/agent/indexer.ts +323 -0
  171. package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
  172. package/renderer/lib/api-docs/agent/types.ts +116 -0
  173. package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
  174. package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
  175. package/renderer/lib/api-docs/auth/crypto.ts +89 -0
  176. package/renderer/lib/api-docs/auth/index.ts +4 -0
  177. package/renderer/lib/api-docs/code-editor/db.ts +164 -0
  178. package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
  179. package/renderer/lib/api-docs/code-editor/index.ts +6 -0
  180. package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
  181. package/renderer/lib/api-docs/code-editor/types.ts +105 -0
  182. package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
  183. package/renderer/lib/api-docs/codegen/har.ts +251 -0
  184. package/renderer/lib/api-docs/codegen/index.ts +159 -0
  185. package/renderer/lib/api-docs/factories.ts +151 -0
  186. package/renderer/lib/api-docs/index.ts +17 -0
  187. package/renderer/lib/api-docs/mobile-context.tsx +112 -0
  188. package/renderer/lib/api-docs/navigation-context.tsx +88 -0
  189. package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
  190. package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
  191. package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
  192. package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
  193. package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
  194. package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
  195. package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
  196. package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
  197. package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
  198. package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
  199. package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
  200. package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
  201. package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
  202. package/renderer/lib/api-docs/playground/context.tsx +107 -0
  203. package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
  204. package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
  205. package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
  206. package/renderer/lib/api-docs/playground/types.ts +35 -0
  207. package/renderer/lib/api-docs/types.ts +269 -0
  208. package/renderer/lib/api-docs/utils.ts +311 -0
  209. package/renderer/lib/cache.ts +193 -0
  210. package/renderer/lib/docs/config/index.ts +29 -0
  211. package/renderer/lib/docs/config/loader.ts +142 -0
  212. package/renderer/lib/docs/config/schema.ts +298 -0
  213. package/renderer/lib/docs/index.ts +12 -0
  214. package/renderer/lib/docs/mdx/compiler.ts +176 -0
  215. package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
  216. package/renderer/lib/docs/mdx/index.ts +26 -0
  217. package/renderer/lib/docs/navigation/generator.ts +348 -0
  218. package/renderer/lib/docs/navigation/index.ts +12 -0
  219. package/renderer/lib/docs/navigation/types.ts +123 -0
  220. package/renderer/lib/docs-navigation-context.tsx +80 -0
  221. package/renderer/lib/multi-tenant/context.ts +105 -0
  222. package/renderer/lib/storage/blob.ts +845 -0
  223. package/renderer/lib/utils.ts +6 -0
  224. package/renderer/next.config.ts +76 -0
  225. package/renderer/package.json +66 -0
  226. package/renderer/postcss.config.mjs +5 -0
  227. package/renderer/public/assets/images/screenshot.png +0 -0
  228. package/renderer/public/assets/logo/dark.svg +9 -0
  229. package/renderer/public/assets/logo/light.svg +9 -0
  230. package/renderer/public/assets/logo.svg +9 -0
  231. package/renderer/public/file.svg +1 -0
  232. package/renderer/public/globe.svg +1 -0
  233. package/renderer/public/icon.png +0 -0
  234. package/renderer/public/logo.svg +9 -0
  235. package/renderer/public/window.svg +1 -0
  236. package/renderer/tsconfig.json +28 -0
  237. package/templates/basic/README.md +139 -0
  238. package/templates/basic/assets/favicon.svg +4 -0
  239. package/templates/basic/assets/logo.svg +9 -0
  240. package/templates/basic/docs.json +47 -0
  241. package/templates/basic/guides/configuration.mdx +149 -0
  242. package/templates/basic/guides/overview.mdx +96 -0
  243. package/templates/basic/index.mdx +39 -0
  244. package/templates/basic/package.json +14 -0
  245. package/templates/basic/quickstart.mdx +92 -0
  246. package/templates/basic/vercel.json +6 -0
  247. package/templates/graphql/README.md +139 -0
  248. package/templates/graphql/api-reference/schema.graphql +305 -0
  249. package/templates/graphql/assets/favicon.svg +4 -0
  250. package/templates/graphql/assets/logo.svg +9 -0
  251. package/templates/graphql/docs.json +54 -0
  252. package/templates/graphql/guides/configuration.mdx +149 -0
  253. package/templates/graphql/guides/overview.mdx +96 -0
  254. package/templates/graphql/index.mdx +39 -0
  255. package/templates/graphql/package.json +14 -0
  256. package/templates/graphql/quickstart.mdx +92 -0
  257. package/templates/graphql/vercel.json +6 -0
  258. package/templates/openapi/README.md +139 -0
  259. package/templates/openapi/api-reference/openapi.json +419 -0
  260. package/templates/openapi/assets/favicon.svg +4 -0
  261. package/templates/openapi/assets/logo.svg +9 -0
  262. package/templates/openapi/docs.json +61 -0
  263. package/templates/openapi/guides/configuration.mdx +149 -0
  264. package/templates/openapi/guides/overview.mdx +96 -0
  265. package/templates/openapi/index.mdx +39 -0
  266. package/templates/openapi/package.json +14 -0
  267. package/templates/openapi/quickstart.mdx +92 -0
  268. package/templates/openapi/vercel.json +6 -0
@@ -0,0 +1,1283 @@
1
+ 'use client'
2
+
3
+ import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'
4
+ import Editor, { type OnMount } from '@monaco-editor/react'
5
+ import type { editor } from 'monaco-editor'
6
+ import {
7
+ File,
8
+ Folder,
9
+ FolderOpen,
10
+ X,
11
+ FloppyDisk,
12
+ CaretRight,
13
+ CaretDown,
14
+ Spinner,
15
+ Plus,
16
+ Minus,
17
+ Eye,
18
+ Code,
19
+ Trash,
20
+ ArrowsOut,
21
+ ArrowClockwise,
22
+ } from '@phosphor-icons/react'
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipTrigger,
27
+ } from '@/components/ui/tooltip'
28
+ import { Button } from '@/components/ui/button'
29
+ import { cn } from '@/lib/utils'
30
+ import {
31
+ useModeContext,
32
+ useWorkspace,
33
+ useNotes,
34
+ useEditorState,
35
+ getMonacoLanguage,
36
+ getFileType,
37
+ supportsPreview,
38
+ type NoteFile,
39
+ } from '@/lib/api-docs/code-editor'
40
+
41
+ // VSCode-style file icon with color coding
42
+ function FileIcon({ path, className }: { path: string; className?: string }) {
43
+ const type = getFileType(path)
44
+
45
+ const colorMap: Record<string, string> = {
46
+ 'javascript': 'text-yellow-400',
47
+ 'typescript': 'text-blue-400',
48
+ 'python': 'text-green-500',
49
+ 'go': 'text-cyan-400',
50
+ 'ruby': 'text-red-400',
51
+ 'php': 'text-purple-400',
52
+ 'shell': 'text-green-400',
53
+ 'markdown': 'text-blue-300',
54
+ 'mermaid': 'text-pink-400',
55
+ 'json': 'text-yellow-500',
56
+ 'yaml': 'text-red-300',
57
+ 'text': 'text-zinc-400',
58
+ }
59
+
60
+ return (
61
+ <File className={cn('h-4 w-4 flex-shrink-0', colorMap[type] || 'text-zinc-400', className)} weight="fill" />
62
+ )
63
+ }
64
+
65
+ // Build tree from flat file list
66
+ interface TreeNode {
67
+ name: string
68
+ path: string
69
+ isFolder: boolean
70
+ children?: TreeNode[]
71
+ file?: NoteFile
72
+ }
73
+
74
+ function buildFileTree(files: NoteFile[]): TreeNode[] {
75
+ const root: TreeNode[] = []
76
+
77
+ // Filter out .folder placeholder files - they're only used internally to create folder structure
78
+ const visibleFiles = files.filter(f => !f.path.endsWith('.folder') && !f.path.includes('/.folder'))
79
+ const sortedFiles = [...visibleFiles].sort((a, b) => a.path.localeCompare(b.path))
80
+
81
+ for (const file of sortedFiles) {
82
+ const parts = file.path.split('/')
83
+ let current = root
84
+
85
+ for (let i = 0; i < parts.length; i++) {
86
+ const part = parts[i]
87
+ const isLastPart = i === parts.length - 1
88
+ const currentPath = parts.slice(0, i + 1).join('/')
89
+
90
+ let existing = current.find(n => n.name === part)
91
+
92
+ if (!existing) {
93
+ existing = {
94
+ name: part,
95
+ path: currentPath,
96
+ isFolder: !isLastPart,
97
+ children: isLastPart ? undefined : [],
98
+ file: isLastPart ? file : undefined,
99
+ }
100
+ current.push(existing)
101
+ }
102
+
103
+ if (!isLastPart && existing.children) {
104
+ current = existing.children
105
+ }
106
+ }
107
+ }
108
+
109
+ // Sort: folders first, then alphabetically
110
+ const sortNodes = (nodes: TreeNode[]): TreeNode[] => {
111
+ return nodes.sort((a, b) => {
112
+ if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1
113
+ return a.name.localeCompare(b.name)
114
+ }).map(node => ({
115
+ ...node,
116
+ children: node.children ? sortNodes(node.children) : undefined,
117
+ }))
118
+ }
119
+
120
+ return sortNodes(root)
121
+ }
122
+
123
+ // File tree item component
124
+ interface FileTreeItemProps {
125
+ node: TreeNode
126
+ depth: number
127
+ activeFile: string | null
128
+ expandedFolders: Set<string>
129
+ onFileClick: (path: string) => void
130
+ onFolderToggle: (path: string) => void
131
+ onDelete?: (path: string) => void
132
+ }
133
+
134
+ function FileTreeItem({
135
+ node,
136
+ depth,
137
+ activeFile,
138
+ expandedFolders,
139
+ onFileClick,
140
+ onFolderToggle,
141
+ onDelete,
142
+ }: FileTreeItemProps) {
143
+ const isExpanded = expandedFolders.has(node.path)
144
+ const isActive = activeFile === node.path
145
+ const [showContextMenu, setShowContextMenu] = useState<{ x: number; y: number } | null>(null)
146
+
147
+ // Close context menu
148
+ useEffect(() => {
149
+ if (showContextMenu) {
150
+ const handleClick = () => setShowContextMenu(null)
151
+ const handleKeyDown = (e: KeyboardEvent) => {
152
+ if (e.key === 'Escape') setShowContextMenu(null)
153
+ }
154
+ document.addEventListener('click', handleClick)
155
+ document.addEventListener('keydown', handleKeyDown)
156
+ return () => {
157
+ document.removeEventListener('click', handleClick)
158
+ document.removeEventListener('keydown', handleKeyDown)
159
+ }
160
+ }
161
+ }, [showContextMenu])
162
+
163
+ const handleClick = (e: React.MouseEvent) => {
164
+ e.preventDefault()
165
+ if (node.isFolder) {
166
+ onFolderToggle(node.path)
167
+ } else {
168
+ onFileClick(node.path)
169
+ }
170
+ }
171
+
172
+ const handleContextMenu = (e: React.MouseEvent) => {
173
+ e.preventDefault()
174
+ setShowContextMenu({ x: e.clientX, y: e.clientY })
175
+ }
176
+
177
+ return (
178
+ <div className="relative">
179
+ <button
180
+ onClick={handleClick}
181
+ onContextMenu={handleContextMenu}
182
+ className={cn(
183
+ 'w-full flex items-center gap-1 text-[13px] text-left transition-colors group',
184
+ isActive && 'sandbox-file-active',
185
+ !isActive && 'sandbox-file-hover',
186
+ )}
187
+ style={{
188
+ height: '22px',
189
+ paddingLeft: `${depth * 8 + 4}px`,
190
+ paddingRight: '8px',
191
+ }}
192
+ >
193
+ <span className="w-4 h-4 flex items-center justify-center flex-shrink-0">
194
+ {node.isFolder && (
195
+ isExpanded ? (
196
+ <CaretDown className="h-3 w-3 text-zinc-400" weight="bold" />
197
+ ) : (
198
+ <CaretRight className="h-3 w-3 text-zinc-400" weight="bold" />
199
+ )
200
+ )}
201
+ </span>
202
+
203
+ {node.isFolder ? (
204
+ isExpanded ? (
205
+ <FolderOpen className="h-4 w-4 text-amber-400 flex-shrink-0" weight="fill" />
206
+ ) : (
207
+ <Folder className="h-4 w-4 text-amber-400 flex-shrink-0" weight="fill" />
208
+ )
209
+ ) : (
210
+ <FileIcon path={node.path} />
211
+ )}
212
+
213
+ <span className={cn(
214
+ 'truncate ml-1',
215
+ isActive && 'text-white',
216
+ !isActive && 'text-zinc-300',
217
+ )}>
218
+ {node.name}
219
+ </span>
220
+ </button>
221
+
222
+ {/* Context menu */}
223
+ {showContextMenu && !node.isFolder && (
224
+ <div
225
+ className="fixed z-50 min-w-[140px] py-1 sandbox-context-menu rounded shadow-xl"
226
+ style={{ left: showContextMenu.x, top: showContextMenu.y }}
227
+ onClick={(e) => e.stopPropagation()}
228
+ >
229
+ <button
230
+ onClick={() => { onDelete?.(node.path); setShowContextMenu(null) }}
231
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-[13px] sandbox-context-menu-delete text-left"
232
+ >
233
+ <Trash className="h-4 w-4" />
234
+ Delete
235
+ </button>
236
+ </div>
237
+ )}
238
+
239
+ {node.isFolder && isExpanded && node.children && (
240
+ <div>
241
+ {node.children.map(child => (
242
+ <FileTreeItem
243
+ key={child.path}
244
+ node={child}
245
+ depth={depth + 1}
246
+ activeFile={activeFile}
247
+ expandedFolders={expandedFolders}
248
+ onFileClick={onFileClick}
249
+ onFolderToggle={onFolderToggle}
250
+ onDelete={onDelete}
251
+ />
252
+ ))}
253
+ </div>
254
+ )}
255
+ </div>
256
+ )
257
+ }
258
+
259
+ // Editor tabs
260
+ function EditorTabs({
261
+ tabs,
262
+ activeTab,
263
+ onTabClick,
264
+ onTabClose
265
+ }: {
266
+ tabs: string[]
267
+ activeTab: string | null
268
+ onTabClick: (path: string) => void
269
+ onTabClose: (path: string) => void
270
+ }) {
271
+ if (tabs.length === 0) return null
272
+
273
+ return (
274
+ <div className="flex items-center h-[35px] sandbox-tabs overflow-x-auto">
275
+ {tabs.map(path => {
276
+ const fileName = path.split('/').pop() || path
277
+ const isActive = path === activeTab
278
+
279
+ return (
280
+ <div
281
+ key={path}
282
+ className={cn(
283
+ 'relative flex items-center gap-1.5 px-3 h-full text-[13px] cursor-pointer group min-w-0',
284
+ isActive ? 'sandbox-tab-active' : 'sandbox-tab-inactive',
285
+ )}
286
+ onClick={() => onTabClick(path)}
287
+ >
288
+ {isActive && <div className="absolute top-0 left-0 right-0 h-[2px] sandbox-tab-indicator" />}
289
+ <FileIcon path={path} className="h-4 w-4 flex-shrink-0" />
290
+ <span className="truncate">{fileName}</span>
291
+ <button
292
+ onClick={(e) => { e.stopPropagation(); onTabClose(path) }}
293
+ className={cn(
294
+ 'p-0.5 rounded flex-shrink-0 ml-1 sandbox-tab-close',
295
+ isActive ? '' : 'opacity-0 group-hover:opacity-100',
296
+ )}
297
+ >
298
+ <X className="h-3 w-3" />
299
+ </button>
300
+ </div>
301
+ )
302
+ })}
303
+ <div className="flex-1 h-full sandbox-tabs" />
304
+ </div>
305
+ )
306
+ }
307
+
308
+ // Note type templates
309
+ const noteTemplates = [
310
+ {
311
+ ext: '.py',
312
+ name: 'Python',
313
+ color: 'text-green-400',
314
+ bg: 'bg-green-500/10 hover:bg-green-500/20 border-green-500/30',
315
+ desc: 'Python script'
316
+ },
317
+ {
318
+ ext: '.ts',
319
+ name: 'TypeScript',
320
+ color: 'text-blue-400',
321
+ bg: 'bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30',
322
+ desc: 'TypeScript code'
323
+ },
324
+ {
325
+ ext: '.js',
326
+ name: 'JavaScript',
327
+ color: 'text-yellow-400',
328
+ bg: 'bg-yellow-500/10 hover:bg-yellow-500/20 border-yellow-500/30',
329
+ desc: 'JavaScript code'
330
+ },
331
+ {
332
+ ext: '.md',
333
+ name: 'Markdown',
334
+ color: 'text-blue-300',
335
+ bg: 'bg-blue-400/10 hover:bg-blue-400/20 border-blue-400/30',
336
+ desc: 'Documentation'
337
+ },
338
+ {
339
+ ext: '.mmd',
340
+ name: 'Mermaid',
341
+ color: 'text-pink-400',
342
+ bg: 'bg-pink-500/10 hover:bg-pink-500/20 border-pink-500/30',
343
+ desc: 'Diagrams'
344
+ },
345
+ {
346
+ ext: '.json',
347
+ name: 'JSON',
348
+ color: 'text-yellow-500',
349
+ bg: 'bg-yellow-500/10 hover:bg-yellow-500/20 border-yellow-500/30',
350
+ desc: 'Data/Config'
351
+ },
352
+ {
353
+ ext: '.go',
354
+ name: 'Go',
355
+ color: 'text-cyan-400',
356
+ bg: 'bg-cyan-500/10 hover:bg-cyan-500/20 border-cyan-500/30',
357
+ desc: 'Go code'
358
+ },
359
+ {
360
+ ext: '.rb',
361
+ name: 'Ruby',
362
+ color: 'text-red-400',
363
+ bg: 'bg-red-500/10 hover:bg-red-500/20 border-red-500/30',
364
+ desc: 'Ruby script'
365
+ },
366
+ ]
367
+
368
+ // New file dialog - VSCode style command palette
369
+ function NewNoteDialog({
370
+ open,
371
+ onOpenChange,
372
+ onCreateNote
373
+ }: {
374
+ open: boolean
375
+ onOpenChange: (open: boolean) => void
376
+ onCreateNote: (path: string) => void
377
+ }) {
378
+ const [fileName, setFileName] = useState('')
379
+ const [selectedTemplate, setSelectedTemplate] = useState<typeof noteTemplates[0] | null>(null)
380
+ const inputRef = useRef<HTMLInputElement>(null)
381
+
382
+ // Reset on open
383
+ useEffect(() => {
384
+ if (open) {
385
+ setFileName('')
386
+ setSelectedTemplate(null)
387
+ setTimeout(() => inputRef.current?.focus(), 50)
388
+ }
389
+ }, [open])
390
+
391
+ // Detect file type from name
392
+ const detectedType = useMemo(() => {
393
+ const ext = fileName.match(/\.[^.]+$/)?.[0]
394
+ return noteTemplates.find(t => t.ext === ext) || null
395
+ }, [fileName])
396
+
397
+ const handleCreate = () => {
398
+ if (!fileName.trim()) return
399
+ let finalName = fileName.trim()
400
+
401
+ // Add extension if not present and template selected
402
+ if (selectedTemplate && !finalName.includes('.')) {
403
+ finalName += selectedTemplate.ext
404
+ }
405
+
406
+ onCreateNote(finalName)
407
+ setFileName('')
408
+ setSelectedTemplate(null)
409
+ onOpenChange(false)
410
+ }
411
+
412
+ const handleTemplateClick = (template: typeof noteTemplates[0]) => {
413
+ setSelectedTemplate(template)
414
+ // Update file name with new extension
415
+ const baseName = fileName.replace(/\.[^.]+$/, '') || 'untitled'
416
+ setFileName(baseName + template.ext)
417
+ inputRef.current?.focus()
418
+ }
419
+
420
+ const handleKeyDown = (e: React.KeyboardEvent) => {
421
+ if (e.key === 'Enter' && fileName.trim()) {
422
+ handleCreate()
423
+ }
424
+ if (e.key === 'Escape') {
425
+ onOpenChange(false)
426
+ }
427
+ }
428
+
429
+ const activeTemplate = detectedType || selectedTemplate
430
+
431
+ if (!open) return null
432
+
433
+ return (
434
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[15vh]">
435
+ {/* Backdrop */}
436
+ <div
437
+ className="absolute inset-0 bg-black/50"
438
+ onClick={() => onOpenChange(false)}
439
+ />
440
+
441
+ {/* Dialog */}
442
+ <div className="relative w-full max-w-lg mx-4 bg-[#1e1e1e] border border-[#454545] rounded-lg shadow-2xl overflow-hidden">
443
+ {/* Input area */}
444
+ <div className="flex items-center gap-3 p-3 border-b border-[#3c3c3c]">
445
+ <div className={cn(
446
+ "w-8 h-8 rounded flex items-center justify-center flex-shrink-0 transition-colors",
447
+ activeTemplate ? activeTemplate.bg : "bg-[#2d2d2d]"
448
+ )}>
449
+ <File className={cn("h-4 w-4", activeTemplate?.color || "text-zinc-400")} weight="fill" />
450
+ </div>
451
+ <input
452
+ ref={inputRef}
453
+ type="text"
454
+ value={fileName}
455
+ onChange={(e) => setFileName(e.target.value)}
456
+ onKeyDown={handleKeyDown}
457
+ placeholder="Enter file name..."
458
+ className="flex-1 bg-transparent text-sm text-zinc-100 placeholder:text-zinc-500 outline-none"
459
+ />
460
+ {fileName.trim() && (
461
+ <button
462
+ onClick={handleCreate}
463
+ className="px-3 py-1.5 text-xs font-medium bg-[#007acc] hover:bg-[#006bb3] text-white rounded transition-colors"
464
+ >
465
+ Create
466
+ </button>
467
+ )}
468
+ </div>
469
+
470
+ {/* Type selection grid */}
471
+ <div className="p-3">
472
+ <p className="text-[11px] uppercase tracking-wider text-zinc-500 mb-2 px-1">
473
+ Select type
474
+ </p>
475
+ <div className="grid grid-cols-4 gap-2">
476
+ {noteTemplates.map((template) => {
477
+ const isActive = activeTemplate?.ext === template.ext
478
+ return (
479
+ <button
480
+ key={template.ext}
481
+ onClick={() => handleTemplateClick(template)}
482
+ className={cn(
483
+ "flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all",
484
+ isActive
485
+ ? `${template.bg} border-current`
486
+ : "bg-[#2d2d2d] border-[#3c3c3c] hover:bg-[#333333] hover:border-[#4c4c4c]"
487
+ )}
488
+ >
489
+ <File className={cn("h-5 w-5", template.color)} weight="fill" />
490
+ <span className={cn(
491
+ "text-[11px] font-medium",
492
+ isActive ? template.color : "text-zinc-300"
493
+ )}>
494
+ {template.name}
495
+ </span>
496
+ </button>
497
+ )
498
+ })}
499
+ </div>
500
+ </div>
501
+
502
+ {/* Tips */}
503
+ <div className="px-4 py-2.5 bg-[#252526] border-t border-[#3c3c3c] flex items-center justify-between">
504
+ <p className="text-[11px] text-zinc-500">
505
+ Tip: Use <code className="px-1 py-0.5 bg-[#3c3c3c] rounded text-zinc-400">folder/file.ext</code> for nested files
506
+ </p>
507
+ <div className="flex items-center gap-2 text-[11px] text-zinc-500">
508
+ <span className="px-1.5 py-0.5 bg-[#3c3c3c] rounded text-[10px]">↵</span>
509
+ <span>Create</span>
510
+ <span className="px-1.5 py-0.5 bg-[#3c3c3c] rounded text-[10px]">Esc</span>
511
+ <span>Cancel</span>
512
+ </div>
513
+ </div>
514
+ </div>
515
+ </div>
516
+ )
517
+ }
518
+
519
+ // Error Boundary for Mermaid Preview
520
+ class MermaidErrorBoundary extends React.Component<
521
+ { children: React.ReactNode; code: string },
522
+ { hasError: boolean; error: string | null }
523
+ > {
524
+ constructor(props: { children: React.ReactNode; code: string }) {
525
+ super(props)
526
+ this.state = { hasError: false, error: null }
527
+ }
528
+
529
+ static getDerivedStateFromError(error: Error) {
530
+ return { hasError: true, error: error.message }
531
+ }
532
+
533
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
534
+ console.error('[MermaidErrorBoundary] Error:', error, errorInfo)
535
+ }
536
+
537
+ // Reset error when code changes
538
+ componentDidUpdate(prevProps: { code: string }) {
539
+ if (prevProps.code !== this.props.code && this.state.hasError) {
540
+ this.setState({ hasError: false, error: null })
541
+ }
542
+ }
543
+
544
+ render() {
545
+ if (this.state.hasError) {
546
+ return (
547
+ <div className="flex flex-col items-center justify-center h-full text-zinc-400 text-sm p-4 gap-4">
548
+ <p className="text-red-400">Render Error: {this.state.error}</p>
549
+ <div className="w-full max-w-lg">
550
+ <p className="text-xs text-zinc-500 mb-2">Source:</p>
551
+ <pre className="p-4 bg-[#2d2d2d] rounded text-xs overflow-auto max-h-[200px] text-zinc-300">
552
+ {this.props.code}
553
+ </pre>
554
+ </div>
555
+ </div>
556
+ )
557
+ }
558
+ return this.props.children
559
+ }
560
+ }
561
+
562
+ // Mermaid preview component with zoom/pan
563
+ function MermaidPreviewInner({ code }: { code: string }) {
564
+ const containerRef = useRef<HTMLDivElement>(null)
565
+ const contentRef = useRef<HTMLDivElement>(null)
566
+ const [svg, setSvg] = useState<string>('')
567
+ const [error, setError] = useState<string | null>(null)
568
+ const [loading, setLoading] = useState(true)
569
+ const renderIdRef = useRef(0)
570
+
571
+ // Zoom and pan state
572
+ const [scale, setScale] = useState(1)
573
+ const [position, setPosition] = useState({ x: 0, y: 0 })
574
+ const [isDragging, setIsDragging] = useState(false)
575
+ const dragStart = useRef({ x: 0, y: 0 })
576
+ const positionStart = useRef({ x: 0, y: 0 })
577
+
578
+ useEffect(() => {
579
+ if (!code.trim()) {
580
+ setLoading(false)
581
+ setSvg('')
582
+ setError(null)
583
+ return
584
+ }
585
+
586
+ const currentRenderId = ++renderIdRef.current
587
+ setLoading(true)
588
+ setError(null)
589
+
590
+ const renderDiagram = async () => {
591
+ try {
592
+ const mermaid = (await import('mermaid')).default
593
+
594
+ mermaid.initialize({
595
+ startOnLoad: false,
596
+ theme: 'dark',
597
+ themeVariables: {
598
+ // Force dark backgrounds everywhere
599
+ background: '#1e1e1e',
600
+ primaryColor: '#3b82f6',
601
+ primaryTextColor: '#ffffff',
602
+ primaryBorderColor: '#60a5fa',
603
+ secondaryColor: '#3b82f6',
604
+ secondaryTextColor: '#ffffff',
605
+ secondaryBorderColor: '#60a5fa',
606
+ tertiaryColor: '#3b82f6',
607
+ tertiaryTextColor: '#ffffff',
608
+ tertiaryBorderColor: '#60a5fa',
609
+
610
+ // Node backgrounds - force blue
611
+ mainBkg: '#3b82f6',
612
+ nodeBkg: '#3b82f6',
613
+ nodeBorder: '#60a5fa',
614
+ nodeTextColor: '#ffffff',
615
+
616
+ // Cluster styling
617
+ clusterBkg: '#27272a',
618
+ clusterBorder: '#52525b',
619
+ titleColor: '#ffffff',
620
+
621
+ // Lines
622
+ lineColor: '#94a3b8',
623
+
624
+ // Font
625
+ fontFamily: 'system-ui, sans-serif',
626
+ fontSize: '14px',
627
+ },
628
+ flowchart: {
629
+ htmlLabels: true,
630
+ useMaxWidth: true,
631
+ }
632
+ })
633
+
634
+ // Use unique ID to avoid conflicts
635
+ const { svg: renderedSvg } = await mermaid.render(`mermaid-${currentRenderId}-${Date.now()}`, code)
636
+
637
+ // Only update if this is still the latest render
638
+ if (currentRenderId === renderIdRef.current) {
639
+ setSvg(renderedSvg)
640
+ setError(null)
641
+ // Reset zoom/pan when diagram changes
642
+ setScale(1)
643
+ setPosition({ x: 0, y: 0 })
644
+ }
645
+ } catch (err: unknown) {
646
+ if (currentRenderId === renderIdRef.current) {
647
+ // Extract meaningful error message from mermaid errors
648
+ let errorMessage = 'Failed to render diagram'
649
+ if (err instanceof Error) {
650
+ // Mermaid errors often have detailed messages
651
+ errorMessage = err.message
652
+ // Clean up common mermaid error prefixes
653
+ if (errorMessage.includes('Parse error')) {
654
+ const match = errorMessage.match(/Parse error on line (\d+)/)
655
+ if (match) {
656
+ errorMessage = `Syntax error on line ${match[1]}`
657
+ }
658
+ }
659
+ } else if (typeof err === 'string') {
660
+ errorMessage = err
661
+ } else if (err && typeof err === 'object' && 'str' in err) {
662
+ // Mermaid sometimes returns {str: "error message"}
663
+ errorMessage = String((err as { str: unknown }).str)
664
+ }
665
+ setError(errorMessage)
666
+ setSvg('') // Clear any previous SVG
667
+ }
668
+ } finally {
669
+ if (currentRenderId === renderIdRef.current) {
670
+ setLoading(false)
671
+ }
672
+ }
673
+ }
674
+
675
+ // Debounce rendering
676
+ const timer = setTimeout(renderDiagram, 300)
677
+ return () => clearTimeout(timer)
678
+ }, [code])
679
+
680
+ // Handle mouse wheel zoom
681
+ const handleWheel = useCallback((e: React.WheelEvent) => {
682
+ e.preventDefault()
683
+ const delta = e.deltaY > 0 ? -0.1 : 0.1
684
+ setScale(prev => Math.min(Math.max(0.25, prev + delta), 3))
685
+ }, [])
686
+
687
+ // Handle mouse down for pan
688
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
689
+ if (e.button !== 0) return // Only left click
690
+ setIsDragging(true)
691
+ dragStart.current = { x: e.clientX, y: e.clientY }
692
+ positionStart.current = position
693
+ }, [position])
694
+
695
+ // Handle mouse move for pan
696
+ const handleMouseMove = useCallback((e: React.MouseEvent) => {
697
+ if (!isDragging) return
698
+ const dx = e.clientX - dragStart.current.x
699
+ const dy = e.clientY - dragStart.current.y
700
+ setPosition({
701
+ x: positionStart.current.x + dx,
702
+ y: positionStart.current.y + dy
703
+ })
704
+ }, [isDragging])
705
+
706
+ // Handle mouse up
707
+ const handleMouseUp = useCallback(() => {
708
+ setIsDragging(false)
709
+ }, [])
710
+
711
+ // Handle zoom controls
712
+ const zoomIn = useCallback(() => setScale(prev => Math.min(prev + 0.25, 3)), [])
713
+ const zoomOut = useCallback(() => setScale(prev => Math.max(prev - 0.25, 0.25)), [])
714
+ const resetView = useCallback(() => {
715
+ setScale(1)
716
+ setPosition({ x: 0, y: 0 })
717
+ }, [])
718
+
719
+ if (loading) {
720
+ return (
721
+ <div className="flex items-center justify-center h-full text-zinc-500 text-sm">
722
+ <Spinner className="h-5 w-5 animate-spin mr-2" />
723
+ Rendering diagram...
724
+ </div>
725
+ )
726
+ }
727
+
728
+ if (error) {
729
+ return (
730
+ <div className="flex flex-col items-center justify-center h-full text-zinc-400 text-sm p-4 gap-4">
731
+ <p className="text-red-400">Syntax Error: {error}</p>
732
+ <div className="w-full max-w-lg">
733
+ <p className="text-xs text-zinc-500 mb-2">Source:</p>
734
+ <pre className="p-4 bg-[#2d2d2d] rounded text-xs overflow-auto max-h-[200px] text-zinc-300">
735
+ {code}
736
+ </pre>
737
+ </div>
738
+ </div>
739
+ )
740
+ }
741
+
742
+ return (
743
+ <div className="relative h-full w-full bg-[#1e1e1e]">
744
+ {/* Zoom controls */}
745
+ <div className="absolute top-3 right-3 z-10 flex items-center gap-1 bg-[#2d2d2d] rounded-md border border-[#3c3c3c] p-1">
746
+ <button
747
+ onClick={zoomOut}
748
+ className="p-1.5 hover:bg-[#3c3c3c] rounded text-zinc-400 hover:text-zinc-200 transition-colors"
749
+ title="Zoom out"
750
+ >
751
+ <Minus className="h-4 w-4" />
752
+ </button>
753
+ <span className="text-xs text-zinc-400 min-w-[3rem] text-center">{Math.round(scale * 100)}%</span>
754
+ <button
755
+ onClick={zoomIn}
756
+ className="p-1.5 hover:bg-[#3c3c3c] rounded text-zinc-400 hover:text-zinc-200 transition-colors"
757
+ title="Zoom in"
758
+ >
759
+ <Plus className="h-4 w-4" />
760
+ </button>
761
+ <div className="w-px h-4 bg-[#3c3c3c] mx-1" />
762
+ <button
763
+ onClick={resetView}
764
+ className="p-1.5 hover:bg-[#3c3c3c] rounded text-zinc-400 hover:text-zinc-200 transition-colors"
765
+ title="Reset view"
766
+ >
767
+ <ArrowsOut className="h-4 w-4" />
768
+ </button>
769
+ </div>
770
+
771
+ {/* Diagram container with zoom/pan */}
772
+ <div
773
+ ref={containerRef}
774
+ className="h-full w-full overflow-hidden cursor-grab active:cursor-grabbing"
775
+ onWheel={handleWheel}
776
+ onMouseDown={handleMouseDown}
777
+ onMouseMove={handleMouseMove}
778
+ onMouseUp={handleMouseUp}
779
+ onMouseLeave={handleMouseUp}
780
+ >
781
+ <div
782
+ ref={contentRef}
783
+ className="h-full w-full flex items-center justify-center [&_svg]:max-w-none"
784
+ style={{
785
+ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
786
+ transformOrigin: 'center center',
787
+ transition: isDragging ? 'none' : 'transform 0.1s ease-out'
788
+ }}
789
+ dangerouslySetInnerHTML={{ __html: svg }}
790
+ />
791
+ </div>
792
+
793
+ {/* Help hint */}
794
+ <div className="absolute bottom-3 left-3 text-[10px] text-zinc-600">
795
+ Scroll to zoom • Drag to pan
796
+ </div>
797
+ </div>
798
+ )
799
+ }
800
+
801
+ // Mermaid preview with error boundary wrapper
802
+ function MermaidPreview({ code }: { code: string }) {
803
+ return (
804
+ <MermaidErrorBoundary code={code}>
805
+ <MermaidPreviewInner code={code} />
806
+ </MermaidErrorBoundary>
807
+ )
808
+ }
809
+
810
+ // Main Notes Mode component
811
+ export function NotesMode({ apiSpecUrl, apiName }: { apiSpecUrl: string; apiName: string }) {
812
+ const { activeFilePath, setActiveFilePath, streamingContent, setStreamingContent, notesRefreshTrigger } = useModeContext()
813
+ const { workspace, loading: workspaceLoading } = useWorkspace(apiSpecUrl, apiName)
814
+ const { notes, loading: notesLoading, createNote, readNote, updateNote, deleteNote, refresh } = useNotes(workspace?.id || null)
815
+ const { state: editorState, openTab, closeTab, setActiveTab, clearAllTabs } = useEditorState(workspace?.id || null)
816
+
817
+ // Editor refs
818
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
819
+
820
+ // Local state
821
+ const [fileContent, setFileContent] = useState('')
822
+ const [isModified, setIsModified] = useState(false)
823
+ const [isSaving, setIsSaving] = useState(false)
824
+ const [isLoadingContent, setIsLoadingContent] = useState(false)
825
+ const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
826
+ const [showNewNote, setShowNewNote] = useState(false)
827
+ const [showPreview, setShowPreview] = useState(false)
828
+
829
+ // Build file tree
830
+ const fileTree = useMemo(() => buildFileTree(notes), [notes])
831
+
832
+ // Refresh notes when triggered by agent (delete, update operations)
833
+ useEffect(() => {
834
+ if (notesRefreshTrigger > 0) {
835
+ refresh()
836
+ }
837
+ }, [notesRefreshTrigger, refresh])
838
+
839
+ // Clear tabs when all notes are deleted
840
+ useEffect(() => {
841
+ if (!notesLoading && notes.length === 0 && editorState?.openTabs && editorState.openTabs.length > 0) {
842
+ clearAllTabs()
843
+ setFileContent('')
844
+ }
845
+ }, [notes.length, notesLoading, editorState?.openTabs, clearAllTabs])
846
+
847
+ // State validation: Clean up orphaned tabs (pointing to deleted files)
848
+ useEffect(() => {
849
+ if (notesLoading || !editorState?.openTabs || editorState.openTabs.length === 0) return
850
+
851
+ const notePaths = new Set(notes.map(n => n.path))
852
+ const orphanedTabs = editorState.openTabs.filter(tab => !notePaths.has(tab))
853
+
854
+ if (orphanedTabs.length > 0) {
855
+ console.log('[NotesMode] Cleaning up orphaned tabs:', orphanedTabs)
856
+ // Close each orphaned tab
857
+ orphanedTabs.forEach(tab => closeTab(tab))
858
+ }
859
+ }, [notes, notesLoading, editorState?.openTabs, closeTab])
860
+
861
+ // State validation: Clear stuck streaming state (no active streaming after 30s)
862
+ useEffect(() => {
863
+ if (!streamingContent?.isStreaming) return
864
+
865
+ const timeout = setTimeout(() => {
866
+ if (streamingContent?.isStreaming) {
867
+ console.log('[NotesMode] Clearing stuck streaming state')
868
+ setStreamingContent(null)
869
+ }
870
+ }, 30000) // 30 second timeout
871
+
872
+ return () => clearTimeout(timeout)
873
+ }, [streamingContent?.isStreaming, setStreamingContent])
874
+
875
+ // Load file content when active tab changes
876
+ useEffect(() => {
877
+ if (!editorState?.activeTab) {
878
+ setFileContent('')
879
+ setIsLoadingContent(false)
880
+ return
881
+ }
882
+
883
+ setIsLoadingContent(true)
884
+ const loadContent = async () => {
885
+ const content = await readNote(editorState.activeTab!)
886
+ setFileContent(content)
887
+ setIsModified(false)
888
+ setIsLoadingContent(false)
889
+ }
890
+
891
+ loadContent()
892
+ }, [editorState?.activeTab, readNote])
893
+
894
+ // Track last processed path to avoid re-processing
895
+ const lastProcessedPath = useRef<string | null>(null)
896
+
897
+ // Handle file navigation from agent
898
+ useEffect(() => {
899
+ if (activeFilePath && workspace) {
900
+ // Strip timestamp from path if present (used to force refresh)
901
+ const cleanPath = activeFilePath.split('?')[0]
902
+ const hasTimestamp = activeFilePath.includes('?t=')
903
+
904
+ // Skip if we've already processed this path (unless it has a timestamp for force refresh)
905
+ if (lastProcessedPath.current === cleanPath && !hasTimestamp) {
906
+ return
907
+ }
908
+ lastProcessedPath.current = cleanPath
909
+
910
+ // Refresh notes first to ensure file is in the list
911
+ refresh().then(async () => {
912
+ openTab(cleanPath)
913
+
914
+ // Auto-enable preview for mermaid files
915
+ if (cleanPath.endsWith('.mmd')) {
916
+ setShowPreview(true)
917
+ }
918
+
919
+ // If this was a write operation (has timestamp), force content reload
920
+ if (hasTimestamp) {
921
+ setIsLoadingContent(true)
922
+ const content = await readNote(cleanPath)
923
+ setFileContent(content)
924
+ setIsModified(false)
925
+ setIsLoadingContent(false)
926
+ // Clear streaming content after file is loaded (prevents flash of empty content)
927
+ if (streamingContent?.path === cleanPath) {
928
+ setStreamingContent(null)
929
+ }
930
+ }
931
+
932
+ // Expand parent folders
933
+ const parts = cleanPath.split('/')
934
+ if (parts.length > 1) {
935
+ const foldersToExpand = parts.slice(0, -1).map((_, i) =>
936
+ parts.slice(0, i + 1).join('/')
937
+ )
938
+ setExpandedFolders(prev => {
939
+ const next = new Set(prev)
940
+ foldersToExpand.forEach(f => next.add(f))
941
+ return next
942
+ })
943
+ }
944
+ })
945
+ }
946
+ }, [activeFilePath, workspace, openTab, refresh, readNote, streamingContent, setStreamingContent])
947
+
948
+ // Handle folder toggle
949
+ const handleFolderToggle = useCallback((path: string) => {
950
+ setExpandedFolders(prev => {
951
+ const next = new Set(prev)
952
+ if (next.has(path)) {
953
+ next.delete(path)
954
+ } else {
955
+ next.add(path)
956
+ }
957
+ return next
958
+ })
959
+ }, [])
960
+
961
+ // Handle content change
962
+ const handleContentChange = useCallback((value: string | undefined) => {
963
+ if (value !== undefined) {
964
+ setFileContent(value)
965
+ setIsModified(true)
966
+ }
967
+ }, [])
968
+
969
+ // Handle save
970
+ const handleSave = useCallback(async () => {
971
+ if (!editorState?.activeTab || !isModified) return
972
+
973
+ setIsSaving(true)
974
+ try {
975
+ await updateNote(editorState.activeTab, fileContent)
976
+ setIsModified(false)
977
+ } finally {
978
+ setIsSaving(false)
979
+ }
980
+ }, [editorState?.activeTab, fileContent, isModified, updateNote])
981
+
982
+ // Keyboard shortcuts
983
+ useEffect(() => {
984
+ const handleKeyDown = (e: KeyboardEvent) => {
985
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
986
+ e.preventDefault()
987
+ handleSave()
988
+ }
989
+ if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
990
+ e.preventDefault()
991
+ setShowNewNote(true)
992
+ }
993
+ }
994
+
995
+ window.addEventListener('keydown', handleKeyDown)
996
+ return () => window.removeEventListener('keydown', handleKeyDown)
997
+ }, [handleSave])
998
+
999
+ // Handle create note
1000
+ const handleCreateNote = useCallback(async (path: string) => {
1001
+ await createNote(path, '')
1002
+ await refresh()
1003
+ await openTab(path)
1004
+ }, [createNote, refresh, openTab])
1005
+
1006
+ // Handle delete note
1007
+ const handleDeleteNote = useCallback(async (path: string) => {
1008
+ await deleteNote(path)
1009
+ closeTab(path)
1010
+ await refresh()
1011
+ }, [deleteNote, closeTab, refresh])
1012
+
1013
+ // Handle reset workspace (clears all state)
1014
+ const handleResetWorkspace = useCallback(async () => {
1015
+ if (!window.confirm('Reset workspace? This will clear all notes and tabs.')) {
1016
+ return
1017
+ }
1018
+
1019
+ try {
1020
+ // Clear all notes from IndexedDB
1021
+ for (const note of notes) {
1022
+ await deleteNote(note.path)
1023
+ }
1024
+
1025
+ // Clear all tabs
1026
+ clearAllTabs()
1027
+
1028
+ // Clear streaming content
1029
+ setStreamingContent(null)
1030
+
1031
+ // Clear file content
1032
+ setFileContent('')
1033
+ setIsModified(false)
1034
+
1035
+ // Refresh notes list
1036
+ await refresh()
1037
+
1038
+ console.log('[NotesMode] Workspace reset complete')
1039
+ } catch (err) {
1040
+ console.error('[NotesMode] Failed to reset workspace:', err)
1041
+ }
1042
+ }, [notes, deleteNote, clearAllTabs, setStreamingContent, refresh])
1043
+
1044
+ // Handle editor mount
1045
+ const handleEditorMount: OnMount = (editor) => {
1046
+ editorRef.current = editor
1047
+ }
1048
+
1049
+ // Check if current file supports preview
1050
+ const currentFileSupportsPreview = editorState?.activeTab ? supportsPreview(editorState.activeTab) : false
1051
+ const currentFileType = editorState?.activeTab ? getFileType(editorState.activeTab) : null
1052
+
1053
+ // Auto-enable preview for mermaid files when tab changes
1054
+ useEffect(() => {
1055
+ if (editorState?.activeTab?.endsWith('.mmd')) {
1056
+ setShowPreview(true)
1057
+ }
1058
+ }, [editorState?.activeTab])
1059
+
1060
+ // Auto-scroll editor to end while streaming
1061
+ useEffect(() => {
1062
+ if (streamingContent?.isStreaming && editorRef.current) {
1063
+ const model = editorRef.current.getModel()
1064
+ if (model) {
1065
+ const lineCount = model.getLineCount()
1066
+ editorRef.current.revealLine(lineCount)
1067
+ // Also position cursor at the end
1068
+ editorRef.current.setPosition({ lineNumber: lineCount, column: 1 })
1069
+ }
1070
+ }
1071
+ }, [streamingContent?.content, streamingContent?.isStreaming])
1072
+
1073
+ // Loading state
1074
+ if (workspaceLoading || notesLoading) {
1075
+ return (
1076
+ <div className="flex items-center justify-center h-full sandbox-bg">
1077
+ <Spinner className="h-6 w-6 animate-spin sandbox-text-muted" />
1078
+ </div>
1079
+ )
1080
+ }
1081
+
1082
+ return (
1083
+ <div className="flex flex-col h-full sandbox-bg">
1084
+ {/* Header */}
1085
+ <div className="flex items-center justify-between px-3 py-1.5 sandbox-header">
1086
+ <div className="flex items-center gap-2">
1087
+ <span className="text-xs sandbox-text-secondary">{workspace?.name || 'Notes'}</span>
1088
+ {isModified && (
1089
+ <span className="text-[11px] sandbox-text-warning font-medium">● Unsaved</span>
1090
+ )}
1091
+ </div>
1092
+
1093
+ <div className="flex items-center gap-1.5">
1094
+ {/* Preview toggle for md/mermaid */}
1095
+ {currentFileSupportsPreview && (
1096
+ <Button
1097
+ variant="ghost"
1098
+ size="sm"
1099
+ onClick={() => setShowPreview(!showPreview)}
1100
+ className={cn(
1101
+ "h-7 px-2 sandbox-btn",
1102
+ showPreview ? "sandbox-btn-active" : ""
1103
+ )}
1104
+ >
1105
+ {showPreview ? <Code className="h-3.5 w-3.5 mr-1" /> : <Eye className="h-3.5 w-3.5 mr-1" />}
1106
+ <span className="text-xs">{showPreview ? 'Code' : 'Preview'}</span>
1107
+ </Button>
1108
+ )}
1109
+
1110
+ <Button
1111
+ variant="ghost"
1112
+ size="sm"
1113
+ onClick={() => setShowNewNote(true)}
1114
+ className="sandbox-btn h-7 px-2"
1115
+ >
1116
+ <Plus className="h-3.5 w-3.5 mr-1" />
1117
+ <span className="text-xs">New</span>
1118
+ </Button>
1119
+
1120
+ <Button
1121
+ variant="ghost"
1122
+ size="sm"
1123
+ onClick={handleSave}
1124
+ disabled={!isModified || isSaving}
1125
+ className="sandbox-btn h-7 px-2 disabled:opacity-40"
1126
+ >
1127
+ <FloppyDisk className="h-3.5 w-3.5 mr-1" />
1128
+ <span className="text-xs">Save</span>
1129
+ </Button>
1130
+
1131
+ {/* Reset button - only show when there are notes or tabs */}
1132
+ {(notes.length > 0 || (editorState?.openTabs?.length ?? 0) > 0) && (
1133
+ <Tooltip>
1134
+ <TooltipTrigger asChild>
1135
+ <Button
1136
+ variant="ghost"
1137
+ size="sm"
1138
+ onClick={handleResetWorkspace}
1139
+ className="sandbox-btn-danger h-7 w-7 p-0"
1140
+ >
1141
+ <ArrowClockwise className="h-3.5 w-3.5" />
1142
+ </Button>
1143
+ </TooltipTrigger>
1144
+ <TooltipContent side="bottom">Reset workspace</TooltipContent>
1145
+ </Tooltip>
1146
+ )}
1147
+ </div>
1148
+ </div>
1149
+
1150
+ {/* Main content */}
1151
+ <div className="flex flex-1 overflow-hidden">
1152
+ {/* Sidebar */}
1153
+ <div className="w-52 flex-shrink-0 border-r border-[#3c3c3c] flex flex-col bg-[#181818]">
1154
+ <div className="px-2.5 py-1.5 border-b border-[#3c3c3c] bg-[#252526]">
1155
+ <span className="text-[11px] font-medium uppercase tracking-wider text-zinc-400">Notes</span>
1156
+ </div>
1157
+
1158
+ <div className="flex-1 overflow-y-auto">
1159
+ {notes.length === 0 ? (
1160
+ <div className="px-3 py-4 text-xs text-zinc-500 text-center">
1161
+ <p className="mb-2">No notes yet</p>
1162
+ <Button
1163
+ variant="link"
1164
+ size="sm"
1165
+ className="text-xs text-blue-400 hover:text-blue-300"
1166
+ onClick={() => setShowNewNote(true)}
1167
+ >
1168
+ Create your first note
1169
+ </Button>
1170
+ </div>
1171
+ ) : (
1172
+ fileTree.map(node => (
1173
+ <FileTreeItem
1174
+ key={node.path}
1175
+ node={node}
1176
+ depth={0}
1177
+ activeFile={editorState?.activeTab || null}
1178
+ expandedFolders={expandedFolders}
1179
+ onFileClick={(path) => {
1180
+ openTab(path)
1181
+ setActiveFilePath(path) // Update URL
1182
+ }}
1183
+ onFolderToggle={handleFolderToggle}
1184
+ onDelete={handleDeleteNote}
1185
+ />
1186
+ ))
1187
+ )}
1188
+ </div>
1189
+ </div>
1190
+
1191
+ {/* Editor area */}
1192
+ <div className="flex-1 flex flex-col overflow-hidden bg-[#1e1e1e]">
1193
+ <EditorTabs
1194
+ tabs={editorState?.openTabs || []}
1195
+ activeTab={editorState?.activeTab || null}
1196
+ onTabClick={(path) => {
1197
+ setActiveTab(path)
1198
+ setActiveFilePath(path) // Update URL
1199
+ }}
1200
+ onTabClose={closeTab}
1201
+ />
1202
+
1203
+ <div className="flex-1 overflow-hidden relative">
1204
+ {isLoadingContent && !streamingContent?.isStreaming && (
1205
+ <div className="absolute inset-0 bg-[#1e1e1e] z-10 flex items-center justify-center">
1206
+ <div className="flex items-center gap-2 text-zinc-400">
1207
+ <Spinner className="h-5 w-5 animate-spin" />
1208
+ <span className="text-sm">Loading content...</span>
1209
+ </div>
1210
+ </div>
1211
+ )}
1212
+ {streamingContent?.isStreaming && streamingContent.path === editorState?.activeTab && (
1213
+ <div className="absolute top-2 right-2 z-10 flex items-center gap-2 px-2 py-1 rounded bg-blue-600/80 text-white text-xs">
1214
+ <Spinner className="h-3 w-3 animate-spin" />
1215
+ <span>AI writing...</span>
1216
+ </div>
1217
+ )}
1218
+ {editorState?.activeTab ? (
1219
+ showPreview && currentFileType === 'mermaid' ? (
1220
+ <MermaidPreview code={streamingContent?.isStreaming && streamingContent.path === editorState.activeTab ? streamingContent.content : fileContent} />
1221
+ ) : (
1222
+ <Editor
1223
+ height="100%"
1224
+ language={getMonacoLanguage(editorState.activeTab)}
1225
+ value={streamingContent?.isStreaming && streamingContent.path === editorState.activeTab ? streamingContent.content : fileContent}
1226
+ onChange={handleContentChange}
1227
+ onMount={handleEditorMount}
1228
+ options={{
1229
+ minimap: { enabled: false },
1230
+ fontSize: 13,
1231
+ fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
1232
+ lineNumbers: 'on',
1233
+ scrollBeyondLastLine: false,
1234
+ wordWrap: 'on',
1235
+ tabSize: 2,
1236
+ automaticLayout: true,
1237
+ padding: { top: 8, bottom: 8 },
1238
+ renderWhitespace: 'selection',
1239
+ bracketPairColorization: { enabled: true },
1240
+ cursorBlinking: 'smooth',
1241
+ smoothScrolling: true,
1242
+ lineHeight: 20,
1243
+ readOnly: streamingContent?.isStreaming && streamingContent.path === editorState.activeTab, // Read-only while streaming
1244
+ }}
1245
+ theme="vs-dark"
1246
+ />
1247
+ )
1248
+ ) : (
1249
+ <div className="flex items-center justify-center h-full">
1250
+ <div className="text-center">
1251
+ <div className="w-16 h-16 mx-auto mb-4 rounded-lg bg-[#2d2d2d] flex items-center justify-center">
1252
+ <File className="h-8 w-8 text-zinc-600" />
1253
+ </div>
1254
+ <p className="text-sm text-zinc-500 mb-1">No file open</p>
1255
+ <p className="text-xs text-zinc-600">Create a note or select one from the sidebar</p>
1256
+ </div>
1257
+ </div>
1258
+ )}
1259
+ </div>
1260
+ </div>
1261
+ </div>
1262
+
1263
+ {/* Status bar */}
1264
+ <div className="flex items-center justify-between h-[22px] px-2.5 border-t border-[#3c3c3c] bg-[#007acc] text-white text-[11px]">
1265
+ <div className="flex items-center gap-3">
1266
+ {editorState?.activeTab && (
1267
+ <span className="opacity-90">{getFileType(editorState.activeTab)}</span>
1268
+ )}
1269
+ </div>
1270
+ <div className="flex items-center gap-3">
1271
+ <span className="opacity-70">{notes.length} note{notes.length !== 1 ? 's' : ''}</span>
1272
+ </div>
1273
+ </div>
1274
+
1275
+ {/* New note dialog */}
1276
+ <NewNoteDialog
1277
+ open={showNewNote}
1278
+ onOpenChange={setShowNewNote}
1279
+ onCreateNote={handleCreateNote}
1280
+ />
1281
+ </div>
1282
+ )
1283
+ }