bingocode 1.0.1 → 1.0.3
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/bin/bingo-win.cjs +34 -3
- package/desktop/README.md +30 -0
- package/desktop/bunfig.toml +1 -0
- package/desktop/index.html +17 -0
- package/desktop/package.json +55 -0
- package/desktop/pnpm-lock.yaml +3832 -0
- package/desktop/public/app-icon.jpg +0 -0
- package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/inter-latin.woff2 +0 -0
- package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
- package/desktop/public/fonts/manrope-latin.woff2 +0 -0
- package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
- package/desktop/public/icons/bilibili.svg +1 -0
- package/desktop/public/icons/douyin.svg +1 -0
- package/desktop/public/icons/github.svg +3 -0
- package/desktop/public/icons/xiaohongshu.svg +1 -0
- package/desktop/scripts/build-macos-arm64.sh +270 -0
- package/desktop/scripts/build-sidecars.ts +183 -0
- package/desktop/scripts/build-windows-x64.ps1 +295 -0
- package/desktop/scripts/scan-missing-imports.ts +235 -0
- package/desktop/sidecars/claude-sidecar.ts +156 -0
- package/desktop/src/App.tsx +5 -0
- package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
- package/desktop/src/__tests__/pages.test.tsx +290 -0
- package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
- package/desktop/src/api/adapters.ts +12 -0
- package/desktop/src/api/agents.ts +36 -0
- package/desktop/src/api/cliTasks.ts +28 -0
- package/desktop/src/api/client.ts +63 -0
- package/desktop/src/api/computerUse.ts +76 -0
- package/desktop/src/api/filesystem.ts +30 -0
- package/desktop/src/api/hahaOAuth.ts +38 -0
- package/desktop/src/api/models.ts +28 -0
- package/desktop/src/api/providers.ts +63 -0
- package/desktop/src/api/search.ts +29 -0
- package/desktop/src/api/sessions.ts +56 -0
- package/desktop/src/api/settings.ts +20 -0
- package/desktop/src/api/skills.ts +19 -0
- package/desktop/src/api/tasks.ts +36 -0
- package/desktop/src/api/teams.ts +44 -0
- package/desktop/src/api/websocket.ts +164 -0
- package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
- package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
- package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
- package/desktop/src/components/chat/ChatInput.tsx +622 -0
- package/desktop/src/components/chat/CodeViewer.tsx +161 -0
- package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
- package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
- package/desktop/src/components/chat/DiffViewer.tsx +157 -0
- package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
- package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
- package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
- package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
- package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
- package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
- package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
- package/desktop/src/components/chat/MessageList.test.tsx +313 -0
- package/desktop/src/components/chat/MessageList.tsx +249 -0
- package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
- package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
- package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
- package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
- package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
- package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
- package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
- package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
- package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
- package/desktop/src/components/chat/UserMessage.tsx +38 -0
- package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
- package/desktop/src/components/chat/clipboard.ts +25 -0
- package/desktop/src/components/chat/composerUtils.test.ts +55 -0
- package/desktop/src/components/chat/composerUtils.ts +149 -0
- package/desktop/src/components/controls/ModelSelector.tsx +156 -0
- package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
- package/desktop/src/components/layout/AppShell.tsx +107 -0
- package/desktop/src/components/layout/ContentRouter.tsx +27 -0
- package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
- package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
- package/desktop/src/components/layout/Sidebar.tsx +384 -0
- package/desktop/src/components/layout/StatusBar.tsx +31 -0
- package/desktop/src/components/layout/TabBar.test.tsx +136 -0
- package/desktop/src/components/layout/TabBar.tsx +318 -0
- package/desktop/src/components/layout/TitleBar.tsx +96 -0
- package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
- package/desktop/src/components/layout/WindowControls.tsx +89 -0
- package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
- package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
- package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
- package/desktop/src/components/shared/Button.tsx +63 -0
- package/desktop/src/components/shared/CopyButton.tsx +58 -0
- package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
- package/desktop/src/components/shared/Dropdown.tsx +91 -0
- package/desktop/src/components/shared/Input.tsx +38 -0
- package/desktop/src/components/shared/Modal.tsx +65 -0
- package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
- package/desktop/src/components/shared/Spinner.tsx +30 -0
- package/desktop/src/components/shared/Textarea.tsx +38 -0
- package/desktop/src/components/shared/Toast.tsx +47 -0
- package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
- package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
- package/desktop/src/components/skills/SkillDetail.tsx +403 -0
- package/desktop/src/components/skills/SkillList.tsx +254 -0
- package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
- package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
- package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
- package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
- package/desktop/src/components/tasks/TaskList.tsx +46 -0
- package/desktop/src/components/tasks/TaskRow.tsx +253 -0
- package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
- package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
- package/desktop/src/config/providerPresets.ts +78 -0
- package/desktop/src/config/spinnerVerbs.ts +193 -0
- package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
- package/desktop/src/i18n/index.ts +54 -0
- package/desktop/src/i18n/locales/en.ts +670 -0
- package/desktop/src/i18n/locales/zh.ts +670 -0
- package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
- package/desktop/src/lib/cronDescribe.ts +188 -0
- package/desktop/src/lib/desktopRuntime.ts +54 -0
- package/desktop/src/lib/parseRunOutput.ts +79 -0
- package/desktop/src/main.tsx +13 -0
- package/desktop/src/mocks/data.ts +202 -0
- package/desktop/src/pages/ActiveSession.test.tsx +181 -0
- package/desktop/src/pages/ActiveSession.tsx +219 -0
- package/desktop/src/pages/AdapterSettings.tsx +375 -0
- package/desktop/src/pages/AgentTeams.tsx +200 -0
- package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
- package/desktop/src/pages/EmptySession.tsx +518 -0
- package/desktop/src/pages/NewTaskModal.tsx +346 -0
- package/desktop/src/pages/ScheduledTasks.tsx +66 -0
- package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
- package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
- package/desktop/src/pages/SessionControls.tsx +460 -0
- package/desktop/src/pages/Settings.tsx +1448 -0
- package/desktop/src/pages/ToolInspection.tsx +235 -0
- package/desktop/src/stores/adapterStore.ts +106 -0
- package/desktop/src/stores/agentStore.ts +34 -0
- package/desktop/src/stores/chatStore.test.ts +505 -0
- package/desktop/src/stores/chatStore.ts +850 -0
- package/desktop/src/stores/cliTaskStore.ts +152 -0
- package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
- package/desktop/src/stores/hahaOAuthStore.ts +97 -0
- package/desktop/src/stores/providerStore.ts +101 -0
- package/desktop/src/stores/sessionStore.test.ts +63 -0
- package/desktop/src/stores/sessionStore.ts +102 -0
- package/desktop/src/stores/settingsStore.ts +120 -0
- package/desktop/src/stores/skillStore.ts +51 -0
- package/desktop/src/stores/tabStore.ts +169 -0
- package/desktop/src/stores/taskStore.ts +68 -0
- package/desktop/src/stores/teamStore.ts +344 -0
- package/desktop/src/stores/uiStore.ts +100 -0
- package/desktop/src/stores/updateStore.test.ts +71 -0
- package/desktop/src/stores/updateStore.ts +221 -0
- package/desktop/src/theme/globals.css +465 -0
- package/desktop/src/types/adapter.ts +33 -0
- package/desktop/src/types/chat.ts +152 -0
- package/desktop/src/types/cliTask.ts +24 -0
- package/desktop/src/types/provider.ts +62 -0
- package/desktop/src/types/session.ts +27 -0
- package/desktop/src/types/settings.ts +22 -0
- package/desktop/src/types/skill.ts +38 -0
- package/desktop/src/types/task.ts +56 -0
- package/desktop/src/types/team.ts +38 -0
- package/desktop/src-tauri/Cargo.lock +5549 -0
- package/desktop/src-tauri/Cargo.toml +20 -0
- package/desktop/src-tauri/app-icon.svg +13 -0
- package/desktop/src-tauri/build.rs +3 -0
- package/desktop/src-tauri/capabilities/default.json +106 -0
- package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
- package/desktop/src-tauri/icons/icon.icns +0 -0
- package/desktop/src-tauri/icons/icon.ico +0 -0
- package/desktop/src-tauri/src/lib.rs +408 -0
- package/desktop/src-tauri/src/main.rs +6 -0
- package/desktop/src-tauri/tauri.conf.json +78 -0
- package/desktop/src-tauri/tauri.macos.conf.json +18 -0
- package/desktop/src-tauri/tauri.release-ci.json +5 -0
- package/desktop/src-tauri/tauri.windows.conf.json +16 -0
- package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
- package/desktop/tsconfig.json +25 -0
- package/desktop/vite.config.ts +26 -0
- package/desktop/vitest.config.ts +18 -0
- package/package.json +1 -1
- package/src/commands/desktop/desktop.tsx +9 -0
- package/src/commands/desktop/index.ts +26 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
|
|
5
|
+
const { initializeMock, renderMock } = vi.hoisted(() => ({
|
|
6
|
+
initializeMock: vi.fn(),
|
|
7
|
+
renderMock: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
vi.mock('mermaid', () => ({
|
|
11
|
+
default: {
|
|
12
|
+
initialize: initializeMock,
|
|
13
|
+
render: renderMock,
|
|
14
|
+
},
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { MermaidRenderer } from './MermaidRenderer'
|
|
18
|
+
|
|
19
|
+
describe('MermaidRenderer', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
initializeMock.mockReset()
|
|
22
|
+
renderMock.mockReset()
|
|
23
|
+
renderMock.mockResolvedValue({
|
|
24
|
+
svg: '<svg viewBox="0 0 200 100"><rect width="200" height="100"></rect></svg>',
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('opens preview with zoom controls and updates the zoom label', async () => {
|
|
29
|
+
render(<MermaidRenderer code={'graph TB\nA-->B'} />)
|
|
30
|
+
|
|
31
|
+
const previewButton = await screen.findByRole('button', { name: /preview/i })
|
|
32
|
+
expect(previewButton).toBeInTheDocument()
|
|
33
|
+
|
|
34
|
+
fireEvent.click(previewButton)
|
|
35
|
+
|
|
36
|
+
await screen.findByText('Mermaid Diagram')
|
|
37
|
+
expect(screen.getByRole('button', { name: 'Zoom in' })).toBeInTheDocument()
|
|
38
|
+
expect(screen.getByRole('button', { name: 'Zoom out' })).toBeInTheDocument()
|
|
39
|
+
|
|
40
|
+
const zoomButton = screen.getByRole('button', { name: '100%' })
|
|
41
|
+
expect(zoomButton).toBeInTheDocument()
|
|
42
|
+
|
|
43
|
+
fireEvent.click(screen.getByRole('button', { name: 'Zoom in' }))
|
|
44
|
+
|
|
45
|
+
await waitFor(() => {
|
|
46
|
+
expect(screen.getByRole('button', { name: '125%' })).toBeInTheDocument()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
fireEvent.click(screen.getByRole('button', { name: '125%' }))
|
|
50
|
+
|
|
51
|
+
await waitFor(() => {
|
|
52
|
+
expect(screen.getByRole('button', { name: '100%' })).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('enters and exits dragging state while panning the preview viewport', async () => {
|
|
57
|
+
render(<MermaidRenderer code={'graph TB\nA-->B'} />)
|
|
58
|
+
|
|
59
|
+
fireEvent.click(await screen.findByRole('button', { name: /preview/i }))
|
|
60
|
+
const viewport = await screen.findByTestId('mermaid-preview-viewport')
|
|
61
|
+
const canvas = screen.getByLabelText('Mermaid preview canvas')
|
|
62
|
+
|
|
63
|
+
Object.defineProperty(viewport, 'setPointerCapture', {
|
|
64
|
+
value: vi.fn(),
|
|
65
|
+
configurable: true,
|
|
66
|
+
})
|
|
67
|
+
Object.defineProperty(viewport, 'releasePointerCapture', {
|
|
68
|
+
value: vi.fn(),
|
|
69
|
+
configurable: true,
|
|
70
|
+
})
|
|
71
|
+
Object.defineProperty(viewport, 'scrollLeft', {
|
|
72
|
+
value: 0,
|
|
73
|
+
writable: true,
|
|
74
|
+
configurable: true,
|
|
75
|
+
})
|
|
76
|
+
Object.defineProperty(viewport, 'scrollTop', {
|
|
77
|
+
value: 0,
|
|
78
|
+
writable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
fireEvent.pointerDown(viewport, {
|
|
83
|
+
pointerId: 7,
|
|
84
|
+
clientX: 180,
|
|
85
|
+
clientY: 120,
|
|
86
|
+
pageX: 180,
|
|
87
|
+
pageY: 120,
|
|
88
|
+
button: 0,
|
|
89
|
+
pointerType: 'mouse',
|
|
90
|
+
})
|
|
91
|
+
expect(canvas).toHaveAttribute('data-dragging', 'true')
|
|
92
|
+
expect(viewport).toHaveStyle({ cursor: 'grabbing' })
|
|
93
|
+
|
|
94
|
+
fireEvent.pointerUp(viewport, { pointerId: 7, pointerType: 'mouse' })
|
|
95
|
+
expect(canvas).toHaveAttribute('data-dragging', 'false')
|
|
96
|
+
expect(viewport).toHaveStyle({ cursor: 'grab' })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
2
|
+
import DOMPurify from 'dompurify'
|
|
3
|
+
import mermaid from 'mermaid'
|
|
4
|
+
import { Modal } from '../shared/Modal'
|
|
5
|
+
import { CopyButton } from '../shared/CopyButton'
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
code: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let mermaidInitialized = false
|
|
12
|
+
const MIN_PREVIEW_ZOOM = 0.5
|
|
13
|
+
const MAX_PREVIEW_ZOOM = 3
|
|
14
|
+
const PREVIEW_ZOOM_STEP = 0.25
|
|
15
|
+
|
|
16
|
+
type SvgMetrics = {
|
|
17
|
+
width: number
|
|
18
|
+
height: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type DragState = {
|
|
22
|
+
pointerId: number
|
|
23
|
+
startX: number
|
|
24
|
+
startY: number
|
|
25
|
+
scrollLeft: number
|
|
26
|
+
scrollTop: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function initMermaid() {
|
|
30
|
+
if (mermaidInitialized) return
|
|
31
|
+
mermaid.initialize({
|
|
32
|
+
startOnLoad: false,
|
|
33
|
+
theme: 'default',
|
|
34
|
+
securityLevel: 'strict',
|
|
35
|
+
fontFamily: 'var(--font-sans)',
|
|
36
|
+
})
|
|
37
|
+
mermaidInitialized = true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let mermaidIdCounter = 0
|
|
41
|
+
|
|
42
|
+
function clampZoom(value: number) {
|
|
43
|
+
return Math.min(MAX_PREVIEW_ZOOM, Math.max(MIN_PREVIEW_ZOOM, value))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPointerPosition(
|
|
47
|
+
event: Pick<React.PointerEvent<HTMLDivElement>, 'clientX' | 'clientY' | 'pageX' | 'pageY'>,
|
|
48
|
+
) {
|
|
49
|
+
const x = Number.isFinite(event.clientX) ? event.clientX : event.pageX
|
|
50
|
+
const y = Number.isFinite(event.clientY) ? event.clientY : event.pageY
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
x: Number.isFinite(x) ? x : 0,
|
|
54
|
+
y: Number.isFinite(y) ? y : 0,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseSvgMetrics(svg: string): SvgMetrics | null {
|
|
59
|
+
const viewBoxMatch = svg.match(/viewBox="([^"]+)"/i)
|
|
60
|
+
if (viewBoxMatch) {
|
|
61
|
+
const viewBox = viewBoxMatch[1]
|
|
62
|
+
if (!viewBox) return null
|
|
63
|
+
|
|
64
|
+
const values = viewBox
|
|
65
|
+
.split(/[\s,]+/)
|
|
66
|
+
.map((part) => Number.parseFloat(part))
|
|
67
|
+
|
|
68
|
+
if (values.length === 4 && values.every((value) => Number.isFinite(value))) {
|
|
69
|
+
const [, , width, height] = values
|
|
70
|
+
if (width !== undefined && height !== undefined) {
|
|
71
|
+
return { width, height }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const widthMatch = svg.match(/\bwidth="([0-9.]+)(?:px)?"/i)
|
|
77
|
+
const heightMatch = svg.match(/\bheight="([0-9.]+)(?:px)?"/i)
|
|
78
|
+
if (widthMatch && heightMatch) {
|
|
79
|
+
const widthValue = widthMatch[1]
|
|
80
|
+
const heightValue = heightMatch[1]
|
|
81
|
+
if (!widthValue || !heightValue) return null
|
|
82
|
+
|
|
83
|
+
const width = Number.parseFloat(widthValue)
|
|
84
|
+
const height = Number.parseFloat(heightValue)
|
|
85
|
+
if (Number.isFinite(width) && Number.isFinite(height)) {
|
|
86
|
+
return { width, height }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function MermaidRenderer({ code }: Props) {
|
|
94
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
95
|
+
const previewViewportRef = useRef<HTMLDivElement>(null)
|
|
96
|
+
const previewContentRef = useRef<HTMLDivElement>(null)
|
|
97
|
+
const dragStateRef = useRef<DragState | null>(null)
|
|
98
|
+
const [svg, setSvg] = useState<string | null>(null)
|
|
99
|
+
const [error, setError] = useState<string | null>(null)
|
|
100
|
+
const [previewOpen, setPreviewOpen] = useState(false)
|
|
101
|
+
const [previewZoom, setPreviewZoom] = useState(1)
|
|
102
|
+
const [isDraggingPreview, setIsDraggingPreview] = useState(false)
|
|
103
|
+
|
|
104
|
+
const svgMetrics = svg ? parseSvgMetrics(svg) : null
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
let cancelled = false
|
|
108
|
+
initMermaid()
|
|
109
|
+
|
|
110
|
+
const id = `mermaid-${++mermaidIdCounter}`
|
|
111
|
+
|
|
112
|
+
mermaid.render(id, code).then(
|
|
113
|
+
({ svg: renderedSvg }) => {
|
|
114
|
+
if (!cancelled) {
|
|
115
|
+
setSvg(renderedSvg)
|
|
116
|
+
setError(null)
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
(err) => {
|
|
120
|
+
if (!cancelled) {
|
|
121
|
+
setError(String(err?.message || err))
|
|
122
|
+
setSvg(null)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return () => { cancelled = true }
|
|
128
|
+
}, [code])
|
|
129
|
+
|
|
130
|
+
const handlePreview = useCallback(() => setPreviewOpen(true), [])
|
|
131
|
+
const handlePreviewClose = useCallback(() => setPreviewOpen(false), [])
|
|
132
|
+
const zoomIn = useCallback(
|
|
133
|
+
() => setPreviewZoom((value) => clampZoom(value + PREVIEW_ZOOM_STEP)),
|
|
134
|
+
[],
|
|
135
|
+
)
|
|
136
|
+
const zoomOut = useCallback(
|
|
137
|
+
() => setPreviewZoom((value) => clampZoom(value - PREVIEW_ZOOM_STEP)),
|
|
138
|
+
[],
|
|
139
|
+
)
|
|
140
|
+
const resetZoom = useCallback(() => setPreviewZoom(1), [])
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!previewOpen) {
|
|
144
|
+
setPreviewZoom(1)
|
|
145
|
+
setIsDraggingPreview(false)
|
|
146
|
+
dragStateRef.current = null
|
|
147
|
+
}
|
|
148
|
+
}, [previewOpen, svg])
|
|
149
|
+
|
|
150
|
+
const stopDraggingPreview = useCallback(() => {
|
|
151
|
+
const viewport = previewViewportRef.current
|
|
152
|
+
const dragState = dragStateRef.current
|
|
153
|
+
if (viewport && dragState) {
|
|
154
|
+
try {
|
|
155
|
+
viewport.releasePointerCapture(dragState.pointerId)
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore capture release failures from synthetic test events.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
dragStateRef.current = null
|
|
161
|
+
setIsDraggingPreview(false)
|
|
162
|
+
}, [])
|
|
163
|
+
|
|
164
|
+
useEffect(() => stopDraggingPreview, [stopDraggingPreview])
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!previewOpen || !previewContentRef.current) return
|
|
168
|
+
|
|
169
|
+
const renderedSvg = previewContentRef.current.querySelector('svg')
|
|
170
|
+
if (!renderedSvg) return
|
|
171
|
+
|
|
172
|
+
renderedSvg.setAttribute('width', '100%')
|
|
173
|
+
renderedSvg.setAttribute('height', '100%')
|
|
174
|
+
renderedSvg.style.width = '100%'
|
|
175
|
+
renderedSvg.style.height = '100%'
|
|
176
|
+
renderedSvg.style.display = 'block'
|
|
177
|
+
}, [previewOpen, svg, previewZoom])
|
|
178
|
+
|
|
179
|
+
const handlePreviewWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
|
|
180
|
+
if (!event.ctrlKey && !event.metaKey) return
|
|
181
|
+
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
const direction = event.deltaY < 0 ? PREVIEW_ZOOM_STEP : -PREVIEW_ZOOM_STEP
|
|
184
|
+
setPreviewZoom((value) => clampZoom(value + direction))
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const handlePreviewPointerDown = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
188
|
+
if (event.pointerType === 'mouse' && event.button !== 0) return
|
|
189
|
+
|
|
190
|
+
const viewport = previewViewportRef.current
|
|
191
|
+
if (!viewport) return
|
|
192
|
+
const { x, y } = getPointerPosition(event)
|
|
193
|
+
|
|
194
|
+
dragStateRef.current = {
|
|
195
|
+
pointerId: event.pointerId,
|
|
196
|
+
startX: x,
|
|
197
|
+
startY: y,
|
|
198
|
+
scrollLeft: viewport.scrollLeft,
|
|
199
|
+
scrollTop: viewport.scrollTop,
|
|
200
|
+
}
|
|
201
|
+
setIsDraggingPreview(true)
|
|
202
|
+
viewport.setPointerCapture(event.pointerId)
|
|
203
|
+
}, [])
|
|
204
|
+
|
|
205
|
+
const handlePreviewPointerMove = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
206
|
+
const viewport = previewViewportRef.current
|
|
207
|
+
const dragState = dragStateRef.current
|
|
208
|
+
if (!viewport || !dragState || dragState.pointerId !== event.pointerId) return
|
|
209
|
+
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
const { x, y } = getPointerPosition(event)
|
|
212
|
+
viewport.scrollLeft = dragState.scrollLeft - (x - dragState.startX)
|
|
213
|
+
viewport.scrollTop = dragState.scrollTop - (y - dragState.startY)
|
|
214
|
+
}, [])
|
|
215
|
+
|
|
216
|
+
const handlePreviewPointerUp = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
|
217
|
+
const dragState = dragStateRef.current
|
|
218
|
+
if (!dragState || dragState.pointerId !== event.pointerId) return
|
|
219
|
+
stopDraggingPreview()
|
|
220
|
+
}, [stopDraggingPreview])
|
|
221
|
+
|
|
222
|
+
const previewCanvasStyle = svgMetrics
|
|
223
|
+
? {
|
|
224
|
+
width: `${svgMetrics.width * previewZoom}px`,
|
|
225
|
+
height: `${svgMetrics.height * previewZoom}px`,
|
|
226
|
+
}
|
|
227
|
+
: undefined
|
|
228
|
+
|
|
229
|
+
if (error) {
|
|
230
|
+
return (
|
|
231
|
+
<div className="my-4 overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-error)]/30">
|
|
232
|
+
<div className="flex items-center gap-2 border-b border-[var(--color-error)]/20 bg-[var(--color-error-container)] px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-[var(--color-error)]">
|
|
233
|
+
<span className="material-symbols-outlined text-[14px]">error</span>
|
|
234
|
+
Mermaid Error
|
|
235
|
+
</div>
|
|
236
|
+
<div className="bg-[var(--color-error-container)]/30 px-3 py-2 font-[var(--font-mono)] text-[11px] text-[var(--color-error)]">
|
|
237
|
+
{error}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!svg) {
|
|
244
|
+
return (
|
|
245
|
+
<div className="my-4 flex items-center justify-center rounded-[var(--radius-lg)] border border-[var(--color-border)]/50 bg-[var(--color-surface-container-low)] py-8">
|
|
246
|
+
<div className="flex items-center gap-2 text-[11px] text-[var(--color-text-tertiary)]">
|
|
247
|
+
<span className="material-symbols-outlined animate-spin text-[16px]">progress_activity</span>
|
|
248
|
+
Rendering diagram...
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<>
|
|
256
|
+
<div className="my-4 overflow-hidden rounded-[var(--radius-lg)] border border-[var(--color-outline-variant)]/50 bg-[var(--color-surface-container-low)]">
|
|
257
|
+
{/* Header */}
|
|
258
|
+
<div className="flex items-center justify-between border-b border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container)] px-3 py-1.5 text-[11px] text-[var(--color-text-tertiary)]">
|
|
259
|
+
<div className="flex items-center gap-2">
|
|
260
|
+
<span className="material-symbols-outlined text-[14px]">account_tree</span>
|
|
261
|
+
<span className="font-semibold uppercase tracking-[0.14em]">Mermaid</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="flex items-center gap-1.5">
|
|
264
|
+
<button
|
|
265
|
+
onClick={handlePreview}
|
|
266
|
+
className="flex items-center gap-1 rounded-md border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] px-2 py-1 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-container-high)] hover:text-[var(--color-text-primary)]"
|
|
267
|
+
>
|
|
268
|
+
<span className="material-symbols-outlined text-[12px]">fullscreen</span>
|
|
269
|
+
Preview
|
|
270
|
+
</button>
|
|
271
|
+
<CopyButton
|
|
272
|
+
text={code}
|
|
273
|
+
className="rounded-md border border-[var(--color-outline-variant)]/40 bg-[var(--color-surface-container-lowest)] px-2 py-1 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-container-high)] hover:text-[var(--color-text-primary)]"
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Diagram */}
|
|
279
|
+
<div
|
|
280
|
+
ref={containerRef}
|
|
281
|
+
className="flex items-center justify-center overflow-auto bg-white p-4 cursor-pointer"
|
|
282
|
+
style={{ maxHeight: 400 }}
|
|
283
|
+
onClick={handlePreview}
|
|
284
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }) }}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* Fullscreen preview modal */}
|
|
289
|
+
<Modal open={previewOpen} onClose={handlePreviewClose} width={1100}>
|
|
290
|
+
<div className="space-y-3">
|
|
291
|
+
<div className="flex items-center justify-between">
|
|
292
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--color-text-primary)]">
|
|
293
|
+
<span className="material-symbols-outlined text-[18px]">account_tree</span>
|
|
294
|
+
Mermaid Diagram
|
|
295
|
+
</div>
|
|
296
|
+
<div className="flex items-center gap-2">
|
|
297
|
+
<div className="flex items-center gap-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-1 py-1">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={zoomOut}
|
|
301
|
+
aria-label="Zoom out"
|
|
302
|
+
className="flex h-8 w-8 items-center justify-center rounded-md text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
|
303
|
+
>
|
|
304
|
+
<span className="material-symbols-outlined text-[16px]">remove</span>
|
|
305
|
+
</button>
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
onClick={resetZoom}
|
|
309
|
+
className="min-w-[68px] rounded-md px-2 py-1 text-[11px] font-semibold text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
|
310
|
+
>
|
|
311
|
+
{Math.round(previewZoom * 100)}%
|
|
312
|
+
</button>
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
onClick={zoomIn}
|
|
316
|
+
aria-label="Zoom in"
|
|
317
|
+
className="flex h-8 w-8 items-center justify-center rounded-md text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
|
318
|
+
>
|
|
319
|
+
<span className="material-symbols-outlined text-[16px]">add</span>
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
<CopyButton
|
|
323
|
+
text={code}
|
|
324
|
+
className="rounded-md border border-[var(--color-border)] px-2.5 py-1 text-[11px] text-[var(--color-text-tertiary)] transition-colors hover:text-[var(--color-text-primary)]"
|
|
325
|
+
/>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<div
|
|
329
|
+
ref={previewViewportRef}
|
|
330
|
+
data-testid="mermaid-preview-viewport"
|
|
331
|
+
className="overflow-auto rounded-xl bg-white"
|
|
332
|
+
style={{
|
|
333
|
+
maxHeight: '75vh',
|
|
334
|
+
cursor: isDraggingPreview ? 'grabbing' : 'grab',
|
|
335
|
+
}}
|
|
336
|
+
onWheel={handlePreviewWheel}
|
|
337
|
+
onPointerDown={handlePreviewPointerDown}
|
|
338
|
+
onPointerMove={handlePreviewPointerMove}
|
|
339
|
+
onPointerUp={handlePreviewPointerUp}
|
|
340
|
+
onPointerCancel={handlePreviewPointerUp}
|
|
341
|
+
onPointerLeave={handlePreviewPointerUp}
|
|
342
|
+
>
|
|
343
|
+
<div className="min-h-full min-w-full p-6">
|
|
344
|
+
<div
|
|
345
|
+
ref={previewContentRef}
|
|
346
|
+
className="mx-auto shrink-0 select-none"
|
|
347
|
+
style={previewCanvasStyle}
|
|
348
|
+
data-dragging={isDraggingPreview ? 'true' : 'false'}
|
|
349
|
+
aria-label="Mermaid preview canvas"
|
|
350
|
+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } }) }}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="text-[11px] text-[var(--color-text-tertiary)]">
|
|
355
|
+
Use the zoom controls to enlarge the diagram. Drag inside the preview to pan, or use the trackpad, mouse wheel, and scrollbars. Hold Ctrl/Command while scrolling to zoom.
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
</Modal>
|
|
359
|
+
</>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CopyButton } from '../shared/CopyButton'
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
copyText?: string
|
|
5
|
+
copyLabel: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function MessageActionBar({
|
|
9
|
+
copyText,
|
|
10
|
+
copyLabel,
|
|
11
|
+
}: Props) {
|
|
12
|
+
const hasCopy = Boolean(copyText?.trim())
|
|
13
|
+
|
|
14
|
+
if (!hasCopy) return null
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="shrink-0 pb-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
|
18
|
+
<CopyButton
|
|
19
|
+
text={copyText!}
|
|
20
|
+
label={copyLabel}
|
|
21
|
+
displayLabel="Copy"
|
|
22
|
+
displayCopiedLabel="Copied"
|
|
23
|
+
className="inline-flex min-h-7 items-center rounded-full border border-[var(--color-border)]/70 bg-[var(--color-surface)] px-2.5 text-[11px] font-medium text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/35 hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
)
|
|
27
|
+
}
|