@mseep/anything-analyzer 3.6.50
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/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import Titlebar from './components/Titlebar'
|
|
4
|
+
import type { AppView } from './components/Titlebar'
|
|
5
|
+
import StatusBar from './components/StatusBar'
|
|
6
|
+
import SessionList from './components/SessionList'
|
|
7
|
+
import BrowserPanel from './components/BrowserPanel'
|
|
8
|
+
import TabBar from './components/TabBar'
|
|
9
|
+
import AnalyzeBar from './components/AnalyzeBar'
|
|
10
|
+
import SettingsModal from './components/SettingsModal'
|
|
11
|
+
import { THEMES, DEFAULT_THEME } from './theme'
|
|
12
|
+
import RequestLog from './components/RequestLog'
|
|
13
|
+
import RequestDetail from './components/RequestDetail'
|
|
14
|
+
import HookLog from './components/HookLog'
|
|
15
|
+
import StorageView from './components/StorageView'
|
|
16
|
+
import ReportView from './components/ReportView'
|
|
17
|
+
import InteractionLog from './components/InteractionLog'
|
|
18
|
+
import { useSession } from './hooks/useSession'
|
|
19
|
+
import { useCapture } from './hooks/useCapture'
|
|
20
|
+
import { useTabs } from './hooks/useTabs'
|
|
21
|
+
import { useConfirm } from './hooks/useConfirm'
|
|
22
|
+
import { useToast } from './ui/Toast'
|
|
23
|
+
|
|
24
|
+
import { LocaleProvider } from './i18n'
|
|
25
|
+
import { zh } from './i18n/zh'
|
|
26
|
+
import { en } from './i18n/en'
|
|
27
|
+
import type { LocaleKey } from './i18n'
|
|
28
|
+
|
|
29
|
+
function App(): React.ReactElement {
|
|
30
|
+
const toast = useToast()
|
|
31
|
+
const { confirm, ConfirmDialog } = useConfirm()
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
sessions,
|
|
35
|
+
currentSessionId,
|
|
36
|
+
currentSession,
|
|
37
|
+
loadSessions,
|
|
38
|
+
createSession,
|
|
39
|
+
selectSession,
|
|
40
|
+
deleteSession,
|
|
41
|
+
startCapture,
|
|
42
|
+
resumeCapture,
|
|
43
|
+
pauseCapture,
|
|
44
|
+
stopCapture
|
|
45
|
+
} = useSession()
|
|
46
|
+
|
|
47
|
+
const { tabs, activeTabId, activeTabUrl, isActiveTabLoading, activateTab, closeTab, createTab } = useTabs()
|
|
48
|
+
|
|
49
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
50
|
+
const [activeView, setActiveView] = useState<AppView>('browser')
|
|
51
|
+
const [createTrigger, setCreateTrigger] = useState(0)
|
|
52
|
+
|
|
53
|
+
// Theme & locale state (persisted to localStorage)
|
|
54
|
+
const [appTheme, setAppTheme] = useState<string>(() => {
|
|
55
|
+
const saved = localStorage.getItem('app-theme')
|
|
56
|
+
return saved && THEMES.some(t => t.id === saved) ? saved : DEFAULT_THEME
|
|
57
|
+
})
|
|
58
|
+
const [appLocale, setAppLocale] = useState<'en' | 'zh'>(() => {
|
|
59
|
+
return (localStorage.getItem('app-locale') as 'en' | 'zh') || 'zh'
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Simple t() for App-level strings (outside LocaleProvider context)
|
|
63
|
+
const localeMaps: Record<string, Record<string, string>> = { zh, en }
|
|
64
|
+
const t = useCallback((key: LocaleKey, vars?: Record<string, string | number>) => {
|
|
65
|
+
let text = localeMaps[appLocale]?.[key] ?? zh[key] ?? key
|
|
66
|
+
if (vars) {
|
|
67
|
+
Object.entries(vars).forEach(([k, v]) => {
|
|
68
|
+
text = text.replace(`{${k}}`, String(v))
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
return text
|
|
72
|
+
}, [appLocale]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
73
|
+
|
|
74
|
+
const handleThemeChange = useCallback((themeId: string) => {
|
|
75
|
+
setAppTheme(themeId)
|
|
76
|
+
localStorage.setItem('app-theme', themeId)
|
|
77
|
+
if (themeId === 'dark') {
|
|
78
|
+
document.documentElement.removeAttribute('data-theme')
|
|
79
|
+
} else {
|
|
80
|
+
document.documentElement.setAttribute('data-theme', themeId)
|
|
81
|
+
}
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
const handleLocaleToggle = useCallback(() => {
|
|
85
|
+
setAppLocale(prev => {
|
|
86
|
+
const next = prev === 'zh' ? 'en' : 'zh'
|
|
87
|
+
localStorage.setItem('app-locale', next)
|
|
88
|
+
return next
|
|
89
|
+
})
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
// Apply theme attribute on mount
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (appTheme === 'dark') {
|
|
95
|
+
document.documentElement.removeAttribute('data-theme')
|
|
96
|
+
} else {
|
|
97
|
+
document.documentElement.setAttribute('data-theme', appTheme)
|
|
98
|
+
}
|
|
99
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
100
|
+
|
|
101
|
+
const openSettings = useCallback(() => {
|
|
102
|
+
setSettingsOpen(true)
|
|
103
|
+
window.electronAPI.setTargetViewVisible(false)
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
const closeSettings = useCallback(() => {
|
|
107
|
+
setSettingsOpen(false)
|
|
108
|
+
if (activeView === 'browser' && currentSession) {
|
|
109
|
+
window.electronAPI.setTargetViewVisible(true)
|
|
110
|
+
}
|
|
111
|
+
}, [activeView, currentSession])
|
|
112
|
+
|
|
113
|
+
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null)
|
|
114
|
+
const [selectedSeqs, setSelectedSeqs] = useState<number[]>([])
|
|
115
|
+
const [activeTab, setActiveTab] = useState('requests')
|
|
116
|
+
|
|
117
|
+
/** Ref to browser placeholder for reporting exact bounds to main process */
|
|
118
|
+
const placeholderRef = useRef<HTMLDivElement>(null)
|
|
119
|
+
|
|
120
|
+
const { requests, hooks, snapshots, reports, interactions, isAnalyzing, analysisError, streamingContent, startAnalysis, cancelAnalysis, chatHistory, isChatting, chatError, sendFollowUp, clearCaptureData } = useCapture(currentSessionId)
|
|
121
|
+
|
|
122
|
+
const selectedRequest = requests.find(r => r.id === selectedRequestId) || null
|
|
123
|
+
|
|
124
|
+
// Navigate browser to session URL when session changes
|
|
125
|
+
// Also enable standalone fingerprint protection
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
setSelectedSeqs([])
|
|
128
|
+
setSelectedRequestId(null)
|
|
129
|
+
const setup = async (): Promise<void> => {
|
|
130
|
+
if (currentSessionId) {
|
|
131
|
+
// Switch to session's isolated partition (hides old tabs, restores/creates new)
|
|
132
|
+
// Navigation to target_url is handled in main process when a blank tab is created
|
|
133
|
+
await window.electronAPI.enableFingerprint(currentSessionId)
|
|
134
|
+
} else {
|
|
135
|
+
await window.electronAPI.disableFingerprint()
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
setup().catch((err) => {
|
|
139
|
+
console.error('Session setup failed:', err)
|
|
140
|
+
})
|
|
141
|
+
}, [currentSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
142
|
+
|
|
143
|
+
// Report exact browser placeholder bounds to main process via ResizeObserver
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const el = placeholderRef.current
|
|
146
|
+
if (!el) return
|
|
147
|
+
|
|
148
|
+
const reportBounds = () => {
|
|
149
|
+
const rect = el.getBoundingClientRect()
|
|
150
|
+
window.electronAPI.syncBrowserBounds({
|
|
151
|
+
x: Math.round(rect.left),
|
|
152
|
+
y: Math.round(rect.top),
|
|
153
|
+
width: Math.round(rect.width),
|
|
154
|
+
height: Math.round(rect.height)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const observer = new ResizeObserver(reportBounds)
|
|
159
|
+
observer.observe(el)
|
|
160
|
+
reportBounds()
|
|
161
|
+
|
|
162
|
+
return () => observer.disconnect()
|
|
163
|
+
}, [])
|
|
164
|
+
|
|
165
|
+
// Hide/show browser view based on active view and session
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (activeView === 'browser' && currentSession) {
|
|
168
|
+
window.electronAPI.setTargetViewVisible(true)
|
|
169
|
+
} else {
|
|
170
|
+
window.electronAPI.setTargetViewVisible(false)
|
|
171
|
+
}
|
|
172
|
+
}, [activeView, currentSession])
|
|
173
|
+
|
|
174
|
+
// Browser navigation handlers
|
|
175
|
+
const handleNavigate = useCallback(async (url: string) => {
|
|
176
|
+
try { await window.electronAPI.navigate(url) } catch (err) { console.error('Navigation failed:', err) }
|
|
177
|
+
}, [])
|
|
178
|
+
|
|
179
|
+
const handleBack = useCallback(async () => {
|
|
180
|
+
try { await window.electronAPI.goBack() } catch (err) { console.error('Go back failed:', err) }
|
|
181
|
+
}, [])
|
|
182
|
+
|
|
183
|
+
const handleForward = useCallback(async () => {
|
|
184
|
+
try { await window.electronAPI.goForward() } catch (err) { console.error('Go forward failed:', err) }
|
|
185
|
+
}, [])
|
|
186
|
+
|
|
187
|
+
const handleReload = useCallback(async () => {
|
|
188
|
+
try { await window.electronAPI.reload() } catch (err) { console.error('Reload failed:', err) }
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
191
|
+
// Analyze handler
|
|
192
|
+
const handleAnalyze = useCallback(async (purpose?: string) => {
|
|
193
|
+
if (!currentSessionId) return
|
|
194
|
+
setActiveView('report')
|
|
195
|
+
await startAnalysis(currentSessionId, purpose, selectedSeqs.length > 0 ? selectedSeqs : undefined)
|
|
196
|
+
}, [currentSessionId, startAnalysis, selectedSeqs])
|
|
197
|
+
|
|
198
|
+
// Cancel analysis handler
|
|
199
|
+
const handleCancelAnalysis = useCallback(async () => {
|
|
200
|
+
if (!currentSessionId) return
|
|
201
|
+
await cancelAnalysis(currentSessionId)
|
|
202
|
+
}, [currentSessionId, cancelAnalysis])
|
|
203
|
+
|
|
204
|
+
// Export requests handler
|
|
205
|
+
const handleExport = useCallback(async () => {
|
|
206
|
+
if (!currentSessionId) return
|
|
207
|
+
try {
|
|
208
|
+
await window.electronAPI.exportRequests(currentSessionId)
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.error('Export failed:', err)
|
|
211
|
+
}
|
|
212
|
+
}, [currentSessionId])
|
|
213
|
+
|
|
214
|
+
// Clear browser environment (with confirmation)
|
|
215
|
+
// Hide native WebContentsView so the confirm dialog is not obscured
|
|
216
|
+
const handleClearEnv = useCallback(async () => {
|
|
217
|
+
window.electronAPI.setTargetViewVisible(false)
|
|
218
|
+
const ok = await confirm(t('data.clearEnvConfirm'), { okText: t('data.clear') })
|
|
219
|
+
if (!ok) {
|
|
220
|
+
if (activeView === 'browser' && currentSession) {
|
|
221
|
+
window.electronAPI.setTargetViewVisible(true)
|
|
222
|
+
}
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
await window.electronAPI.clearBrowserEnv()
|
|
227
|
+
toast.success(t('toast.envCleared'))
|
|
228
|
+
} catch (err) {
|
|
229
|
+
console.error('Clear env failed:', err)
|
|
230
|
+
toast.error(t('toast.envClearFailed'))
|
|
231
|
+
}
|
|
232
|
+
if (activeView === 'browser' && currentSession) {
|
|
233
|
+
window.electronAPI.setTargetViewVisible(true)
|
|
234
|
+
}
|
|
235
|
+
}, [toast, confirm, t, activeView, currentSession])
|
|
236
|
+
|
|
237
|
+
// Clear capture data for re-analysis
|
|
238
|
+
const handleClearData = useCallback(async () => {
|
|
239
|
+
if (!currentSessionId) return
|
|
240
|
+
try {
|
|
241
|
+
await clearCaptureData(currentSessionId)
|
|
242
|
+
setSelectedRequestId(null)
|
|
243
|
+
setSelectedSeqs([])
|
|
244
|
+
toast.success(t('toast.dataCleared'))
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error('Clear data failed:', err)
|
|
247
|
+
toast.error(t('toast.dataClearFailed'))
|
|
248
|
+
}
|
|
249
|
+
}, [currentSessionId, clearCaptureData, toast])
|
|
250
|
+
|
|
251
|
+
const handleFollowUp = useCallback(async (msg: string) => {
|
|
252
|
+
if (!currentSessionId) return
|
|
253
|
+
await sendFollowUp(currentSessionId, msg)
|
|
254
|
+
}, [currentSessionId, sendFollowUp])
|
|
255
|
+
|
|
256
|
+
// Pill button style for capture controls in browser address bar
|
|
257
|
+
const pillStyle: React.CSSProperties = {
|
|
258
|
+
padding: '5px 14px',
|
|
259
|
+
borderRadius: 6,
|
|
260
|
+
fontSize: 'var(--font-size-2xs)',
|
|
261
|
+
cursor: 'pointer',
|
|
262
|
+
border: 'none',
|
|
263
|
+
whiteSpace: 'nowrap',
|
|
264
|
+
lineHeight: 1.2,
|
|
265
|
+
fontWeight: 600,
|
|
266
|
+
fontFamily: 'var(--font-sans)',
|
|
267
|
+
}
|
|
268
|
+
const pillActive: React.CSSProperties = { ...pillStyle, background: 'var(--color-success-bg)', color: 'var(--color-success)', border: '1px solid var(--color-success-border)' }
|
|
269
|
+
const pillDisabled: React.CSSProperties = { ...pillStyle, background: 'var(--color-active)', color: 'var(--text-disabled)', cursor: 'not-allowed' }
|
|
270
|
+
const pillStart: React.CSSProperties = { ...pillStyle, background: 'var(--color-success)', color: '#000', padding: '5px 18px' }
|
|
271
|
+
const pillPause: React.CSSProperties = { ...pillStyle, background: 'var(--color-warning-bg)', color: 'var(--color-warning)', border: '1px solid var(--color-warning-border)' }
|
|
272
|
+
const pillStop: React.CSSProperties = { ...pillStyle, background: 'var(--color-error-bg)', color: 'var(--color-error)', border: '1px solid var(--color-error-border)' }
|
|
273
|
+
const pillPauseActive: React.CSSProperties = { ...pillStyle, background: 'var(--color-warning-bg)', color: 'var(--color-warning)', border: '1px solid var(--color-warning)' }
|
|
274
|
+
|
|
275
|
+
// Build capture slot for BrowserPanel
|
|
276
|
+
const buildCaptureSlot = () => {
|
|
277
|
+
if (!currentSessionId) return null
|
|
278
|
+
if (!currentSession?.status || currentSession.status === 'stopped') {
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<button style={pillStart} onClick={startCapture}>● {t('browser.start')}</button>
|
|
282
|
+
<button style={pillDisabled}>⏸ {t('browser.pause')}</button>
|
|
283
|
+
<button style={pillDisabled}>■ {t('browser.stop')}</button>
|
|
284
|
+
</>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
if (currentSession.status === 'running') {
|
|
288
|
+
return (
|
|
289
|
+
<>
|
|
290
|
+
<button style={pillActive}>● {t('browser.start')}</button>
|
|
291
|
+
<button style={pillPause} onClick={pauseCapture}>⏸ {t('browser.pause')}</button>
|
|
292
|
+
<button style={pillStop} onClick={stopCapture}>■ {t('browser.stop')}</button>
|
|
293
|
+
</>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
if (currentSession.status === 'paused') {
|
|
297
|
+
return (
|
|
298
|
+
<>
|
|
299
|
+
<button style={pillPauseActive}>⏸ {t('browser.pause')}</button>
|
|
300
|
+
<button style={pillStart} onClick={resumeCapture}>▶ {t('browser.resume')}</button>
|
|
301
|
+
<button style={pillStop} onClick={stopCapture}>■ {t('browser.stop')}</button>
|
|
302
|
+
</>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Empty state guide for when no session is selected
|
|
309
|
+
const renderEmptyGuide = () => (
|
|
310
|
+
<div style={{
|
|
311
|
+
flex: 1,
|
|
312
|
+
display: 'flex',
|
|
313
|
+
flexDirection: 'column',
|
|
314
|
+
alignItems: 'center',
|
|
315
|
+
justifyContent: 'center',
|
|
316
|
+
gap: 24,
|
|
317
|
+
padding: 40,
|
|
318
|
+
color: 'var(--text-muted)',
|
|
319
|
+
}}>
|
|
320
|
+
<div style={{ fontSize: 32, fontWeight: 700, color: 'var(--text-secondary)', letterSpacing: '-0.5px' }}>
|
|
321
|
+
{t('session.emptyTitle')}
|
|
322
|
+
</div>
|
|
323
|
+
<div style={{
|
|
324
|
+
display: 'flex',
|
|
325
|
+
flexDirection: 'column',
|
|
326
|
+
gap: 14,
|
|
327
|
+
fontSize: 'var(--font-size-base)',
|
|
328
|
+
lineHeight: 1.6,
|
|
329
|
+
}}>
|
|
330
|
+
{[
|
|
331
|
+
{ step: '1', icon: '+', text: t('session.emptyStep1') },
|
|
332
|
+
{ step: '2', icon: '🌐', text: t('session.emptyStep2') },
|
|
333
|
+
{ step: '3', icon: '●', text: t('session.emptyStep3') },
|
|
334
|
+
{ step: '4', icon: '⚡', text: t('session.emptyStep4') },
|
|
335
|
+
].map(({ step, icon, text }) => (
|
|
336
|
+
<div key={step} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
337
|
+
<span style={{
|
|
338
|
+
width: 28,
|
|
339
|
+
height: 28,
|
|
340
|
+
borderRadius: '50%',
|
|
341
|
+
background: 'var(--color-active)',
|
|
342
|
+
color: 'var(--text-secondary)',
|
|
343
|
+
display: 'flex',
|
|
344
|
+
alignItems: 'center',
|
|
345
|
+
justifyContent: 'center',
|
|
346
|
+
fontSize: 'var(--font-size-sm)',
|
|
347
|
+
fontWeight: 600,
|
|
348
|
+
flexShrink: 0,
|
|
349
|
+
}}>
|
|
350
|
+
{step}
|
|
351
|
+
</span>
|
|
352
|
+
<span style={{ color: 'var(--text-secondary)' }}>{text}</span>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
<button
|
|
357
|
+
style={{
|
|
358
|
+
marginTop: 8,
|
|
359
|
+
padding: '8px 24px',
|
|
360
|
+
borderRadius: 6,
|
|
361
|
+
background: 'var(--text-primary)',
|
|
362
|
+
color: 'var(--color-base)',
|
|
363
|
+
border: 'none',
|
|
364
|
+
fontSize: 'var(--font-size-base)',
|
|
365
|
+
fontWeight: 600,
|
|
366
|
+
cursor: 'pointer',
|
|
367
|
+
fontFamily: 'var(--font-sans)',
|
|
368
|
+
}}
|
|
369
|
+
onClick={() => setCreateTrigger(n => n + 1)}
|
|
370
|
+
>
|
|
371
|
+
{t('session.newSession')}
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
// Render the Browser view — ONLY browser, no data panel
|
|
377
|
+
const renderBrowserView = () => (
|
|
378
|
+
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
|
379
|
+
{currentSession ? (
|
|
380
|
+
<>
|
|
381
|
+
{/* Browser tab bar */}
|
|
382
|
+
<TabBar
|
|
383
|
+
tabs={tabs}
|
|
384
|
+
activeTabId={activeTabId}
|
|
385
|
+
onActivate={activateTab}
|
|
386
|
+
onClose={closeTab}
|
|
387
|
+
onCreate={() => createTab()}
|
|
388
|
+
/>
|
|
389
|
+
|
|
390
|
+
{/* Browser panel - address bar + nav buttons + capture pills */}
|
|
391
|
+
<BrowserPanel
|
|
392
|
+
currentUrl={activeTabUrl}
|
|
393
|
+
isLoading={isActiveTabLoading}
|
|
394
|
+
onNavigate={handleNavigate}
|
|
395
|
+
onBack={handleBack}
|
|
396
|
+
onForward={handleForward}
|
|
397
|
+
onReload={handleReload}
|
|
398
|
+
captureSlot={buildCaptureSlot()}
|
|
399
|
+
onClearEnv={handleClearEnv}
|
|
400
|
+
onToggleDevTools={() => window.electronAPI.toggleDevTools()}
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
{/* Browser view placeholder — native WebContentsView overlays this area */}
|
|
404
|
+
<div
|
|
405
|
+
ref={placeholderRef}
|
|
406
|
+
style={{
|
|
407
|
+
flex: 1,
|
|
408
|
+
position: 'relative',
|
|
409
|
+
minHeight: 80
|
|
410
|
+
}}
|
|
411
|
+
/>
|
|
412
|
+
</>
|
|
413
|
+
) : (
|
|
414
|
+
renderEmptyGuide()
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
// Inspector sub-tab styles
|
|
420
|
+
const inspectorTabStyle: React.CSSProperties = {
|
|
421
|
+
fontSize: 'var(--font-size-2xs)',
|
|
422
|
+
color: 'var(--text-muted)',
|
|
423
|
+
display: 'flex',
|
|
424
|
+
alignItems: 'center',
|
|
425
|
+
cursor: 'pointer',
|
|
426
|
+
letterSpacing: '0.3px',
|
|
427
|
+
padding: '0',
|
|
428
|
+
background: 'none',
|
|
429
|
+
border: 'none',
|
|
430
|
+
fontFamily: 'var(--font-sans)',
|
|
431
|
+
height: '100%',
|
|
432
|
+
boxShadow: 'none',
|
|
433
|
+
transition: 'color 0.15s',
|
|
434
|
+
}
|
|
435
|
+
const inspectorTabActiveStyle: React.CSSProperties = {
|
|
436
|
+
...inspectorTabStyle,
|
|
437
|
+
color: 'var(--text-primary)',
|
|
438
|
+
boxShadow: 'inset 0 -2px 0 var(--text-primary)',
|
|
439
|
+
}
|
|
440
|
+
const inspectorTabCountStyle: React.CSSProperties = {
|
|
441
|
+
fontSize: 'var(--font-size-3xs)',
|
|
442
|
+
color: 'var(--text-muted)',
|
|
443
|
+
marginLeft: 5,
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Render the Inspector view — sub-tabs + left/right split + bottom AnalyzeBar
|
|
447
|
+
const renderInspectorView = () => (
|
|
448
|
+
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
|
449
|
+
{currentSession ? (
|
|
450
|
+
<>
|
|
451
|
+
{/* Sub-tabs: Requests / Hooks / Storage + Capture controls */}
|
|
452
|
+
<div style={{
|
|
453
|
+
height: 36,
|
|
454
|
+
background: 'var(--color-bar)',
|
|
455
|
+
borderBottom: '1px solid var(--color-border)',
|
|
456
|
+
display: 'flex',
|
|
457
|
+
alignItems: 'stretch',
|
|
458
|
+
padding: '0 16px',
|
|
459
|
+
gap: 24,
|
|
460
|
+
flexShrink: 0,
|
|
461
|
+
}}>
|
|
462
|
+
<button
|
|
463
|
+
style={activeTab === 'requests' ? inspectorTabActiveStyle : inspectorTabStyle}
|
|
464
|
+
onClick={() => setActiveTab('requests')}
|
|
465
|
+
>
|
|
466
|
+
{t('data.requests')} <span style={inspectorTabCountStyle}>{requests.length}</span>
|
|
467
|
+
</button>
|
|
468
|
+
<button
|
|
469
|
+
style={activeTab === 'hooks' ? inspectorTabActiveStyle : inspectorTabStyle}
|
|
470
|
+
onClick={() => setActiveTab('hooks')}
|
|
471
|
+
>
|
|
472
|
+
{t('data.hooks')} <span style={inspectorTabCountStyle}>{hooks.length}</span>
|
|
473
|
+
</button>
|
|
474
|
+
<button
|
|
475
|
+
style={activeTab === 'storage' ? inspectorTabActiveStyle : inspectorTabStyle}
|
|
476
|
+
onClick={() => setActiveTab('storage')}
|
|
477
|
+
>
|
|
478
|
+
{t('data.storage')} <span style={inspectorTabCountStyle}>{snapshots.length}</span>
|
|
479
|
+
</button>
|
|
480
|
+
<button
|
|
481
|
+
style={activeTab === 'interactions' ? inspectorTabActiveStyle : inspectorTabStyle}
|
|
482
|
+
onClick={() => setActiveTab('interactions')}
|
|
483
|
+
>
|
|
484
|
+
{t('data.interactions')} <span style={inspectorTabCountStyle}>{interactions.length}</span>
|
|
485
|
+
</button>
|
|
486
|
+
|
|
487
|
+
{/* Spacer */}
|
|
488
|
+
<div style={{ flex: 1 }} />
|
|
489
|
+
|
|
490
|
+
{/* Clear data button */}
|
|
491
|
+
{currentSessionId && requests.length > 0 && (
|
|
492
|
+
<button
|
|
493
|
+
style={{
|
|
494
|
+
fontSize: 'var(--font-size-2xs)',
|
|
495
|
+
color: 'var(--text-muted)',
|
|
496
|
+
background: 'none',
|
|
497
|
+
border: 'none',
|
|
498
|
+
cursor: 'pointer',
|
|
499
|
+
padding: '0 4px',
|
|
500
|
+
fontFamily: 'var(--font-sans)',
|
|
501
|
+
whiteSpace: 'nowrap',
|
|
502
|
+
}}
|
|
503
|
+
onClick={async () => {
|
|
504
|
+
const ok = await confirm(t('data.clearDataConfirm'), {
|
|
505
|
+
okText: t('data.clear'),
|
|
506
|
+
})
|
|
507
|
+
if (ok) handleClearData()
|
|
508
|
+
}}
|
|
509
|
+
title={t('data.clearDataConfirm')}
|
|
510
|
+
>{t('data.clearData')}</button>
|
|
511
|
+
)}
|
|
512
|
+
|
|
513
|
+
{/* Capture controls in Inspector */}
|
|
514
|
+
{currentSessionId && (
|
|
515
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
516
|
+
{buildCaptureSlot()}
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* Tab content */}
|
|
522
|
+
{activeTab === 'requests' ? (
|
|
523
|
+
/* Left-right split: request list (420px) + detail panel */
|
|
524
|
+
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
525
|
+
<div style={{ flex: 1, minWidth: 400, borderRight: '1px solid var(--color-border)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
526
|
+
<RequestLog requests={requests} selectedId={selectedRequestId} onSelect={(r) => setSelectedRequestId(r.id)} selectedSeqs={selectedSeqs} onSelectedSeqsChange={setSelectedSeqs} />
|
|
527
|
+
</div>
|
|
528
|
+
<div style={{ width: 400, minWidth: 320, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
|
529
|
+
<RequestDetail request={selectedRequest} hooks={hooks} />
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
) : activeTab === 'hooks' ? (
|
|
533
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '0 12px' }}>
|
|
534
|
+
<HookLog hooks={hooks} />
|
|
535
|
+
</div>
|
|
536
|
+
) : activeTab === 'storage' ? (
|
|
537
|
+
<div style={{ flex: 1, overflow: 'auto', padding: '0 12px' }}>
|
|
538
|
+
<StorageView snapshots={snapshots} />
|
|
539
|
+
</div>
|
|
540
|
+
) : activeTab === 'interactions' ? (
|
|
541
|
+
<div style={{ flex: 1, overflow: 'hidden', padding: '0 12px' }}>
|
|
542
|
+
<InteractionLog interactions={interactions} />
|
|
543
|
+
</div>
|
|
544
|
+
) : null}
|
|
545
|
+
|
|
546
|
+
{/* Bottom AnalyzeBar */}
|
|
547
|
+
<AnalyzeBar
|
|
548
|
+
onAnalyze={handleAnalyze}
|
|
549
|
+
onExport={handleExport}
|
|
550
|
+
hasRequests={requests.length > 0}
|
|
551
|
+
isAnalyzing={isAnalyzing}
|
|
552
|
+
isStopped={currentSession.status !== 'running'}
|
|
553
|
+
selectedSeqCount={selectedSeqs.length}
|
|
554
|
+
totalCount={requests.length}
|
|
555
|
+
/>
|
|
556
|
+
</>
|
|
557
|
+
) : (
|
|
558
|
+
renderEmptyGuide()
|
|
559
|
+
)}
|
|
560
|
+
</div>
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
// Render the Report view
|
|
564
|
+
const renderReportView = () => (
|
|
565
|
+
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
|
|
566
|
+
{currentSession ? (
|
|
567
|
+
<ReportView
|
|
568
|
+
report={reports[0] || null}
|
|
569
|
+
isAnalyzing={isAnalyzing}
|
|
570
|
+
analysisError={analysisError}
|
|
571
|
+
streamingContent={streamingContent}
|
|
572
|
+
onReAnalyze={handleAnalyze}
|
|
573
|
+
onCancelAnalysis={handleCancelAnalysis}
|
|
574
|
+
chatHistory={chatHistory}
|
|
575
|
+
isChatting={isChatting}
|
|
576
|
+
chatError={chatError}
|
|
577
|
+
onSendFollowUp={handleFollowUp}
|
|
578
|
+
sessionName={currentSession?.name}
|
|
579
|
+
requests={requests}
|
|
580
|
+
hooks={hooks}
|
|
581
|
+
/>
|
|
582
|
+
) : (
|
|
583
|
+
renderEmptyGuide()
|
|
584
|
+
)}
|
|
585
|
+
</div>
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
return (
|
|
589
|
+
<LocaleProvider locale={appLocale}>
|
|
590
|
+
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column', background: 'var(--color-base)' }}>
|
|
591
|
+
{/* Custom Titlebar */}
|
|
592
|
+
<Titlebar
|
|
593
|
+
theme={appTheme}
|
|
594
|
+
onThemeChange={handleThemeChange}
|
|
595
|
+
locale={appLocale}
|
|
596
|
+
onLocaleToggle={handleLocaleToggle}
|
|
597
|
+
activeView={activeView}
|
|
598
|
+
onViewChange={setActiveView}
|
|
599
|
+
requestCount={requests.length}
|
|
600
|
+
/>
|
|
601
|
+
|
|
602
|
+
{/* Main content area: Sidebar + View */}
|
|
603
|
+
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
604
|
+
{/* Left sidebar */}
|
|
605
|
+
<div style={{
|
|
606
|
+
width: 'var(--sidebar-width)',
|
|
607
|
+
minWidth: 220,
|
|
608
|
+
maxWidth: 220,
|
|
609
|
+
borderRight: '1px solid var(--color-border)',
|
|
610
|
+
background: 'var(--color-sidebar)',
|
|
611
|
+
overflow: 'hidden',
|
|
612
|
+
display: 'flex',
|
|
613
|
+
flexDirection: 'column'
|
|
614
|
+
}}>
|
|
615
|
+
<SessionList
|
|
616
|
+
sessions={sessions}
|
|
617
|
+
currentSessionId={currentSessionId}
|
|
618
|
+
onSelect={selectSession}
|
|
619
|
+
onCreate={createSession}
|
|
620
|
+
onDelete={deleteSession}
|
|
621
|
+
onOpenSettings={openSettings}
|
|
622
|
+
activeRequestCount={requests.length}
|
|
623
|
+
createTrigger={createTrigger}
|
|
624
|
+
/>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
{/* Main view area */}
|
|
628
|
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--color-content)' }}>
|
|
629
|
+
{activeView === 'browser' && renderBrowserView()}
|
|
630
|
+
{activeView === 'inspector' && renderInspectorView()}
|
|
631
|
+
{activeView === 'report' && renderReportView()}
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
|
|
635
|
+
{/* Status bar */}
|
|
636
|
+
<StatusBar
|
|
637
|
+
status={currentSession?.status ?? null}
|
|
638
|
+
requestCount={requests.length}
|
|
639
|
+
hookCount={hooks.length}
|
|
640
|
+
interactionCount={interactions.length}
|
|
641
|
+
sessionName={currentSession?.name}
|
|
642
|
+
activeView={activeView}
|
|
643
|
+
llmModel={reports[0]?.llm_model}
|
|
644
|
+
tokenCount={reports[0] ? (reports[0].prompt_tokens ?? 0) + (reports[0].completion_tokens ?? 0) : undefined}
|
|
645
|
+
/>
|
|
646
|
+
|
|
647
|
+
{/* Settings modal */}
|
|
648
|
+
<SettingsModal open={settingsOpen} onClose={closeSettings} currentSessionId={currentSession?.id ?? null} />
|
|
649
|
+
{/* Confirm dialog (portal) */}
|
|
650
|
+
{ConfirmDialog}
|
|
651
|
+
</div>
|
|
652
|
+
</LocaleProvider>
|
|
653
|
+
)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export default App
|