@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.
- package/LICENSE +33 -0
- package/README.md +415 -0
- package/bin/devdoc.js +13 -0
- package/dist/cli/commands/build.d.ts +5 -0
- package/dist/cli/commands/build.js +87 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +143 -0
- package/dist/cli/commands/create.d.ts +24 -0
- package/dist/cli/commands/create.js +387 -0
- package/dist/cli/commands/deploy.d.ts +9 -0
- package/dist/cli/commands/deploy.js +433 -0
- package/dist/cli/commands/dev.d.ts +6 -0
- package/dist/cli/commands/dev.js +139 -0
- package/dist/cli/commands/init.d.ts +11 -0
- package/dist/cli/commands/init.js +238 -0
- package/dist/cli/commands/keys.d.ts +12 -0
- package/dist/cli/commands/keys.js +165 -0
- package/dist/cli/commands/start.d.ts +5 -0
- package/dist/cli/commands/start.js +56 -0
- package/dist/cli/commands/upload.d.ts +13 -0
- package/dist/cli/commands/upload.js +238 -0
- package/dist/cli/commands/whoami.d.ts +8 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/config/index.d.ts +80 -0
- package/dist/config/index.js +133 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/paths.d.ts +16 -0
- package/dist/utils/paths.js +50 -0
- package/package.json +51 -0
- package/renderer/app/api/assets/[...path]/route.ts +123 -0
- package/renderer/app/api/assets/route.ts +124 -0
- package/renderer/app/api/assets/upload/route.ts +177 -0
- package/renderer/app/api/auth-schemes/route.ts +77 -0
- package/renderer/app/api/chat/route.ts +858 -0
- package/renderer/app/api/codegen/route.ts +72 -0
- package/renderer/app/api/collections/route.ts +1016 -0
- package/renderer/app/api/debug/route.ts +53 -0
- package/renderer/app/api/deploy/route.ts +234 -0
- package/renderer/app/api/device/route.ts +42 -0
- package/renderer/app/api/docs/route.ts +187 -0
- package/renderer/app/api/keys/regenerate/route.ts +80 -0
- package/renderer/app/api/openapi-spec/route.ts +151 -0
- package/renderer/app/api/projects/[slug]/route.ts +153 -0
- package/renderer/app/api/projects/[slug]/stats/route.ts +96 -0
- package/renderer/app/api/projects/register/route.ts +152 -0
- package/renderer/app/api/proxy/route.ts +149 -0
- package/renderer/app/api/proxy-stream/route.ts +168 -0
- package/renderer/app/api/redirects/route.ts +47 -0
- package/renderer/app/api/schema/route.ts +65 -0
- package/renderer/app/api/subdomains/check/route.ts +172 -0
- package/renderer/app/api/suggestions/route.ts +144 -0
- package/renderer/app/favicon.ico +0 -0
- package/renderer/app/globals.css +1103 -0
- package/renderer/app/layout.tsx +47 -0
- package/renderer/app/llms-full.txt/route.ts +346 -0
- package/renderer/app/llms.txt/route.ts +279 -0
- package/renderer/app/page.tsx +14 -0
- package/renderer/app/robots.txt/route.ts +84 -0
- package/renderer/app/sitemap.xml/route.ts +199 -0
- package/renderer/components/docs/index.ts +12 -0
- package/renderer/components/docs/mdx/accordion.tsx +169 -0
- package/renderer/components/docs/mdx/badge.tsx +132 -0
- package/renderer/components/docs/mdx/callouts.tsx +154 -0
- package/renderer/components/docs/mdx/cards.tsx +213 -0
- package/renderer/components/docs/mdx/changelog.tsx +120 -0
- package/renderer/components/docs/mdx/code-block.tsx +186 -0
- package/renderer/components/docs/mdx/code-group.tsx +421 -0
- package/renderer/components/docs/mdx/file-embeds.tsx +105 -0
- package/renderer/components/docs/mdx/frame.tsx +112 -0
- package/renderer/components/docs/mdx/highlight.tsx +151 -0
- package/renderer/components/docs/mdx/iframe.tsx +134 -0
- package/renderer/components/docs/mdx/image.tsx +235 -0
- package/renderer/components/docs/mdx/index.ts +204 -0
- package/renderer/components/docs/mdx/mermaid.tsx +240 -0
- package/renderer/components/docs/mdx/param-field.tsx +200 -0
- package/renderer/components/docs/mdx/steps.tsx +113 -0
- package/renderer/components/docs/mdx/tabs.tsx +86 -0
- package/renderer/components/docs/mdx-renderer.tsx +100 -0
- package/renderer/components/docs/navigation/breadcrumbs.tsx +76 -0
- package/renderer/components/docs/navigation/index.ts +8 -0
- package/renderer/components/docs/navigation/page-nav.tsx +64 -0
- package/renderer/components/docs/navigation/sidebar.tsx +515 -0
- package/renderer/components/docs/navigation/toc.tsx +113 -0
- package/renderer/components/docs/notice.tsx +105 -0
- package/renderer/components/docs-header.tsx +274 -0
- package/renderer/components/docs-viewer/agent/agent-chat.tsx +2076 -0
- package/renderer/components/docs-viewer/agent/cards/debug-context-card.tsx +90 -0
- package/renderer/components/docs-viewer/agent/cards/endpoint-context-card.tsx +49 -0
- package/renderer/components/docs-viewer/agent/cards/index.tsx +50 -0
- package/renderer/components/docs-viewer/agent/cards/response-options-card.tsx +212 -0
- package/renderer/components/docs-viewer/agent/cards/types.ts +84 -0
- package/renderer/components/docs-viewer/agent/chat-message.tsx +17 -0
- package/renderer/components/docs-viewer/agent/index.tsx +6 -0
- package/renderer/components/docs-viewer/agent/messages/assistant-message.tsx +119 -0
- package/renderer/components/docs-viewer/agent/messages/chat-message.tsx +46 -0
- package/renderer/components/docs-viewer/agent/messages/index.ts +17 -0
- package/renderer/components/docs-viewer/agent/messages/tool-call-display.tsx +721 -0
- package/renderer/components/docs-viewer/agent/messages/types.ts +61 -0
- package/renderer/components/docs-viewer/agent/messages/typing-indicator.tsx +24 -0
- package/renderer/components/docs-viewer/agent/messages/user-message.tsx +51 -0
- package/renderer/components/docs-viewer/code-editor/index.tsx +2 -0
- package/renderer/components/docs-viewer/code-editor/notes-mode.tsx +1283 -0
- package/renderer/components/docs-viewer/content/changelog-page.tsx +331 -0
- package/renderer/components/docs-viewer/content/doc-page.tsx +285 -0
- package/renderer/components/docs-viewer/content/documentation-viewer.tsx +17 -0
- package/renderer/components/docs-viewer/content/index.tsx +29 -0
- package/renderer/components/docs-viewer/content/introduction.tsx +21 -0
- package/renderer/components/docs-viewer/content/request-details.tsx +330 -0
- package/renderer/components/docs-viewer/content/sections/auth.tsx +69 -0
- package/renderer/components/docs-viewer/content/sections/body.tsx +66 -0
- package/renderer/components/docs-viewer/content/sections/headers.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/overview.tsx +40 -0
- package/renderer/components/docs-viewer/content/sections/parameters.tsx +43 -0
- package/renderer/components/docs-viewer/content/sections/responses.tsx +87 -0
- package/renderer/components/docs-viewer/global-auth-modal.tsx +352 -0
- package/renderer/components/docs-viewer/index.tsx +1466 -0
- package/renderer/components/docs-viewer/playground/auth-editor.tsx +280 -0
- package/renderer/components/docs-viewer/playground/body-editor.tsx +221 -0
- package/renderer/components/docs-viewer/playground/code-editor.tsx +224 -0
- package/renderer/components/docs-viewer/playground/code-snippet.tsx +387 -0
- package/renderer/components/docs-viewer/playground/graphql-playground.tsx +745 -0
- package/renderer/components/docs-viewer/playground/index.tsx +671 -0
- package/renderer/components/docs-viewer/playground/key-value-editor.tsx +261 -0
- package/renderer/components/docs-viewer/playground/method-selector.tsx +60 -0
- package/renderer/components/docs-viewer/playground/request-builder.tsx +179 -0
- package/renderer/components/docs-viewer/playground/request-tabs.tsx +237 -0
- package/renderer/components/docs-viewer/playground/response-cards/idle-card.tsx +21 -0
- package/renderer/components/docs-viewer/playground/response-cards/index.tsx +93 -0
- package/renderer/components/docs-viewer/playground/response-cards/loading-card.tsx +16 -0
- package/renderer/components/docs-viewer/playground/response-cards/network-error-card.tsx +23 -0
- package/renderer/components/docs-viewer/playground/response-cards/response-body-card.tsx +268 -0
- package/renderer/components/docs-viewer/playground/response-cards/types.ts +82 -0
- package/renderer/components/docs-viewer/playground/response-viewer.tsx +43 -0
- package/renderer/components/docs-viewer/search/index.ts +2 -0
- package/renderer/components/docs-viewer/search/search-dialog.tsx +331 -0
- package/renderer/components/docs-viewer/search/use-search.ts +117 -0
- package/renderer/components/docs-viewer/shared/markdown-renderer.tsx +431 -0
- package/renderer/components/docs-viewer/shared/method-badge.tsx +41 -0
- package/renderer/components/docs-viewer/shared/schema-viewer.tsx +349 -0
- package/renderer/components/docs-viewer/sidebar/collection-tree.tsx +239 -0
- package/renderer/components/docs-viewer/sidebar/endpoint-options.tsx +316 -0
- package/renderer/components/docs-viewer/sidebar/index.tsx +343 -0
- package/renderer/components/docs-viewer/sidebar/right-sidebar.tsx +202 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-group.tsx +118 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-item.tsx +226 -0
- package/renderer/components/docs-viewer/sidebar/sidebar-section.tsx +52 -0
- package/renderer/components/theme-provider.tsx +11 -0
- package/renderer/components/theme-toggle.tsx +76 -0
- package/renderer/components/ui/badge.tsx +46 -0
- package/renderer/components/ui/button.tsx +59 -0
- package/renderer/components/ui/dialog.tsx +118 -0
- package/renderer/components/ui/dropdown-menu.tsx +257 -0
- package/renderer/components/ui/input.tsx +21 -0
- package/renderer/components/ui/label.tsx +24 -0
- package/renderer/components/ui/navigation-menu.tsx +168 -0
- package/renderer/components/ui/select.tsx +190 -0
- package/renderer/components/ui/spinner.tsx +114 -0
- package/renderer/components/ui/tabs.tsx +66 -0
- package/renderer/components/ui/tooltip.tsx +61 -0
- package/renderer/hooks/use-code-copy.ts +88 -0
- package/renderer/hooks/use-openapi-title.ts +44 -0
- package/renderer/lib/api-docs/agent/index.ts +6 -0
- package/renderer/lib/api-docs/agent/indexer.ts +323 -0
- package/renderer/lib/api-docs/agent/spec-summary.ts +335 -0
- package/renderer/lib/api-docs/agent/types.ts +116 -0
- package/renderer/lib/api-docs/auth/auth-context.tsx +225 -0
- package/renderer/lib/api-docs/auth/auth-storage.ts +87 -0
- package/renderer/lib/api-docs/auth/crypto.ts +89 -0
- package/renderer/lib/api-docs/auth/index.ts +4 -0
- package/renderer/lib/api-docs/code-editor/db.ts +164 -0
- package/renderer/lib/api-docs/code-editor/hooks.ts +266 -0
- package/renderer/lib/api-docs/code-editor/index.ts +6 -0
- package/renderer/lib/api-docs/code-editor/mode-context.tsx +207 -0
- package/renderer/lib/api-docs/code-editor/types.ts +105 -0
- package/renderer/lib/api-docs/codegen/definitions.ts +297 -0
- package/renderer/lib/api-docs/codegen/har.ts +251 -0
- package/renderer/lib/api-docs/codegen/index.ts +159 -0
- package/renderer/lib/api-docs/factories.ts +151 -0
- package/renderer/lib/api-docs/index.ts +17 -0
- package/renderer/lib/api-docs/mobile-context.tsx +112 -0
- package/renderer/lib/api-docs/navigation-context.tsx +88 -0
- package/renderer/lib/api-docs/parsers/graphql/README.md +129 -0
- package/renderer/lib/api-docs/parsers/graphql/index.ts +91 -0
- package/renderer/lib/api-docs/parsers/graphql/parser.ts +491 -0
- package/renderer/lib/api-docs/parsers/graphql/transformer.ts +246 -0
- package/renderer/lib/api-docs/parsers/graphql/types.ts +283 -0
- package/renderer/lib/api-docs/parsers/openapi/README.md +32 -0
- package/renderer/lib/api-docs/parsers/openapi/dereferencer.ts +60 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/auth.ts +574 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/body.ts +403 -0
- package/renderer/lib/api-docs/parsers/openapi/extractors/index.ts +232 -0
- package/renderer/lib/api-docs/parsers/openapi/index.ts +171 -0
- package/renderer/lib/api-docs/parsers/openapi/transformer.ts +277 -0
- package/renderer/lib/api-docs/parsers/openapi/validator.ts +31 -0
- package/renderer/lib/api-docs/playground/context.tsx +107 -0
- package/renderer/lib/api-docs/playground/navigation-context.tsx +124 -0
- package/renderer/lib/api-docs/playground/request-builder.ts +223 -0
- package/renderer/lib/api-docs/playground/request-runner.ts +282 -0
- package/renderer/lib/api-docs/playground/types.ts +35 -0
- package/renderer/lib/api-docs/types.ts +269 -0
- package/renderer/lib/api-docs/utils.ts +311 -0
- package/renderer/lib/cache.ts +193 -0
- package/renderer/lib/docs/config/index.ts +29 -0
- package/renderer/lib/docs/config/loader.ts +142 -0
- package/renderer/lib/docs/config/schema.ts +298 -0
- package/renderer/lib/docs/index.ts +12 -0
- package/renderer/lib/docs/mdx/compiler.ts +176 -0
- package/renderer/lib/docs/mdx/frontmatter.ts +80 -0
- package/renderer/lib/docs/mdx/index.ts +26 -0
- package/renderer/lib/docs/navigation/generator.ts +348 -0
- package/renderer/lib/docs/navigation/index.ts +12 -0
- package/renderer/lib/docs/navigation/types.ts +123 -0
- package/renderer/lib/docs-navigation-context.tsx +80 -0
- package/renderer/lib/multi-tenant/context.ts +105 -0
- package/renderer/lib/storage/blob.ts +845 -0
- package/renderer/lib/utils.ts +6 -0
- package/renderer/next.config.ts +76 -0
- package/renderer/package.json +66 -0
- package/renderer/postcss.config.mjs +5 -0
- package/renderer/public/assets/images/screenshot.png +0 -0
- package/renderer/public/assets/logo/dark.svg +9 -0
- package/renderer/public/assets/logo/light.svg +9 -0
- package/renderer/public/assets/logo.svg +9 -0
- package/renderer/public/file.svg +1 -0
- package/renderer/public/globe.svg +1 -0
- package/renderer/public/icon.png +0 -0
- package/renderer/public/logo.svg +9 -0
- package/renderer/public/window.svg +1 -0
- package/renderer/tsconfig.json +28 -0
- package/templates/basic/README.md +139 -0
- package/templates/basic/assets/favicon.svg +4 -0
- package/templates/basic/assets/logo.svg +9 -0
- package/templates/basic/docs.json +47 -0
- package/templates/basic/guides/configuration.mdx +149 -0
- package/templates/basic/guides/overview.mdx +96 -0
- package/templates/basic/index.mdx +39 -0
- package/templates/basic/package.json +14 -0
- package/templates/basic/quickstart.mdx +92 -0
- package/templates/basic/vercel.json +6 -0
- package/templates/graphql/README.md +139 -0
- package/templates/graphql/api-reference/schema.graphql +305 -0
- package/templates/graphql/assets/favicon.svg +4 -0
- package/templates/graphql/assets/logo.svg +9 -0
- package/templates/graphql/docs.json +54 -0
- package/templates/graphql/guides/configuration.mdx +149 -0
- package/templates/graphql/guides/overview.mdx +96 -0
- package/templates/graphql/index.mdx +39 -0
- package/templates/graphql/package.json +14 -0
- package/templates/graphql/quickstart.mdx +92 -0
- package/templates/graphql/vercel.json +6 -0
- package/templates/openapi/README.md +139 -0
- package/templates/openapi/api-reference/openapi.json +419 -0
- package/templates/openapi/assets/favicon.svg +4 -0
- package/templates/openapi/assets/logo.svg +9 -0
- package/templates/openapi/docs.json +61 -0
- package/templates/openapi/guides/configuration.mdx +149 -0
- package/templates/openapi/guides/overview.mdx +96 -0
- package/templates/openapi/index.mdx +39 -0
- package/templates/openapi/package.json +14 -0
- package/templates/openapi/quickstart.mdx +92 -0
- 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
|
+
}
|