@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.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,86 @@
1
+ import React, { useEffect, useState, useCallback } from 'react'
2
+ import { useLocale } from '../i18n'
3
+ import { Empty } from '../ui'
4
+ import { AiLogList } from './AiLogList'
5
+ import { AiLogDetail } from './AiLogDetail'
6
+ import type { AiRequestLog } from '@shared/types'
7
+ import styles from './AiLogView.module.css'
8
+
9
+ interface AiLogViewProps {
10
+ sessionId: string
11
+ sessionName?: string
12
+ onBack: () => void
13
+ }
14
+
15
+ export const AiLogView: React.FC<AiLogViewProps> = ({ sessionId, sessionName, onBack }) => {
16
+ const { t } = useLocale()
17
+ const [logs, setLogs] = useState<AiRequestLog[]>([])
18
+ const [selectedId, setSelectedId] = useState<number | null>(null)
19
+ const [isGlobal, setIsGlobal] = useState(false)
20
+
21
+ const loadLogs = useCallback(async () => {
22
+ const data = isGlobal
23
+ ? await window.electronAPI.getAiRequestLogsAll(200, 0)
24
+ : await window.electronAPI.getAiRequestLogs(sessionId)
25
+ setLogs(data)
26
+ // Auto-select first if current selection not in list
27
+ if (data.length > 0 && (selectedId === null || !data.some(l => l.id === selectedId))) {
28
+ setSelectedId(data[0].id)
29
+ }
30
+ if (data.length === 0) setSelectedId(null)
31
+ }, [sessionId, isGlobal, selectedId])
32
+
33
+ useEffect(() => {
34
+ loadLogs()
35
+ }, [sessionId, isGlobal]) // eslint-disable-line react-hooks/exhaustive-deps
36
+
37
+ if (logs.length === 0) {
38
+ return (
39
+ <div className={styles.container}>
40
+ <div className={styles.toolbar}>
41
+ <button className={styles.backBtn} onClick={onBack}>{t('aiLog.backToReport')}</button>
42
+ </div>
43
+ <div className={styles.emptyContainer}>
44
+ <Empty description={t('aiLog.noData')} />
45
+ </div>
46
+ </div>
47
+ )
48
+ }
49
+
50
+ return (
51
+ <div className={styles.container}>
52
+ {/* Toolbar */}
53
+ <div className={styles.toolbar}>
54
+ <button className={styles.backBtn} onClick={onBack}>
55
+ {isGlobal ? t('aiLog.backToSession') : t('aiLog.backToReport')}
56
+ </button>
57
+ <div className={styles.toolbarSpacer} />
58
+ {!isGlobal && sessionName && (
59
+ <span className={styles.sessionLabel}>Session: {sessionName}</span>
60
+ )}
61
+ {isGlobal && (
62
+ <span className={styles.globalLabel}>{t('aiLog.globalMode')}</span>
63
+ )}
64
+ <button
65
+ className={styles.toggleBtn}
66
+ onClick={() => {
67
+ if (isGlobal) {
68
+ setIsGlobal(false)
69
+ } else {
70
+ setIsGlobal(true)
71
+ }
72
+ setSelectedId(null)
73
+ }}
74
+ >
75
+ {isGlobal ? t('aiLog.backToSession') : t('aiLog.viewAllSessions')}
76
+ </button>
77
+ </div>
78
+
79
+ {/* Main content: list + detail */}
80
+ <div className={styles.mainContent}>
81
+ <AiLogList logs={logs} selectedId={selectedId} onSelect={setSelectedId} />
82
+ <AiLogDetail logId={selectedId} />
83
+ </div>
84
+ </div>
85
+ )
86
+ }
@@ -0,0 +1,79 @@
1
+ .analyzeBar {
2
+ height: 42px;
3
+ background: var(--color-sidebar);
4
+ border-top: 1px solid var(--color-border);
5
+ display: flex;
6
+ align-items: center;
7
+ padding: 0 16px;
8
+ gap: 10px;
9
+ flex-shrink: 0;
10
+ }
11
+
12
+ .analyzeSelect {
13
+ padding: 5px 10px;
14
+ border-radius: 6px;
15
+ background: var(--color-surface);
16
+ border: 1px solid var(--color-border-subtle);
17
+ font-size: var(--font-size-3xs);
18
+ color: var(--text-secondary);
19
+ font-family: var(--font-sans);
20
+ cursor: pointer;
21
+ outline: none;
22
+ }
23
+
24
+ .analyzeSelect:focus {
25
+ border-color: var(--color-border-hover);
26
+ }
27
+
28
+ .analyzeBtnPrimary {
29
+ padding: 6px 20px;
30
+ border-radius: 6px;
31
+ background: var(--text-primary);
32
+ color: var(--color-base);
33
+ font-size: var(--font-size-2xs);
34
+ font-weight: 600;
35
+ cursor: pointer;
36
+ border: none;
37
+ font-family: var(--font-sans);
38
+ transition: opacity var(--transition-fast);
39
+ }
40
+
41
+ .analyzeBtnPrimary:hover {
42
+ opacity: 0.9;
43
+ }
44
+
45
+ .analyzeBtnPrimary:disabled {
46
+ opacity: 0.4;
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ .analyzeBtnDim {
51
+ padding: 6px 16px;
52
+ border-radius: 6px;
53
+ background: var(--color-active);
54
+ border: 1px solid var(--color-border-subtle);
55
+ color: var(--text-secondary);
56
+ font-size: var(--font-size-3xs);
57
+ cursor: pointer;
58
+ font-family: var(--font-sans);
59
+ transition: border-color var(--transition-fast), color var(--transition-fast);
60
+ }
61
+
62
+ .analyzeBtnDim:hover {
63
+ border-color: var(--color-border-hover);
64
+ color: var(--text-primary);
65
+ }
66
+
67
+ .analyzeBtnDim:disabled {
68
+ opacity: 0.4;
69
+ cursor: not-allowed;
70
+ }
71
+
72
+ .spacer {
73
+ flex: 1;
74
+ }
75
+
76
+ .info {
77
+ font-size: var(--font-size-3xs);
78
+ color: var(--text-muted);
79
+ }
@@ -0,0 +1,104 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { useLocale } from '../i18n'
3
+ import type { PromptTemplate } from '../../shared/types'
4
+ import styles from './AnalyzeBar.module.css'
5
+
6
+ interface AnalyzeBarProps {
7
+ onAnalyze: (purpose?: string) => void
8
+ onExport: () => void
9
+ hasRequests: boolean
10
+ isAnalyzing: boolean
11
+ isStopped: boolean
12
+ selectedSeqCount: number
13
+ totalCount: number
14
+ }
15
+
16
+ const AnalyzeBar: React.FC<AnalyzeBarProps> = ({
17
+ onAnalyze,
18
+ onExport,
19
+ hasRequests,
20
+ isAnalyzing,
21
+ isStopped,
22
+ selectedSeqCount,
23
+ totalCount,
24
+ }) => {
25
+ const { t } = useLocale()
26
+ const [purposeId, setPurposeId] = useState('auto')
27
+ const [templates, setTemplates] = useState<PromptTemplate[]>([])
28
+
29
+ useEffect(() => {
30
+ window.electronAPI.getPromptTemplates().then(setTemplates).catch(() => {})
31
+ }, [])
32
+
33
+ const handleAnalyze = () => {
34
+ if (purposeId === 'auto') {
35
+ onAnalyze(undefined)
36
+ } else {
37
+ onAnalyze(purposeId)
38
+ }
39
+ }
40
+
41
+ const handleAnalyzeSelected = () => {
42
+ if (purposeId === 'auto') {
43
+ onAnalyze(undefined)
44
+ } else {
45
+ onAnalyze(purposeId)
46
+ }
47
+ }
48
+
49
+ const canAnalyze = isStopped && hasRequests && !isAnalyzing
50
+
51
+ return (
52
+ <div className={styles.analyzeBar}>
53
+ <select
54
+ className={styles.analyzeSelect}
55
+ value={purposeId}
56
+ onChange={(e) => setPurposeId(e.target.value)}
57
+ disabled={isAnalyzing}
58
+ >
59
+ <option value="auto">▾ {t('capture.autoDetect') ?? '自动检测'}</option>
60
+ {templates.map((tpl) => (
61
+ <option key={tpl.id} value={tpl.id}>{tpl.name}</option>
62
+ ))}
63
+ </select>
64
+
65
+ <button
66
+ className={styles.analyzeBtnPrimary}
67
+ disabled={!canAnalyze}
68
+ onClick={handleAnalyze}
69
+ >
70
+ ✦ {t('capture.analyze')}
71
+ </button>
72
+
73
+ {selectedSeqCount > 0 && (
74
+ <button
75
+ className={styles.analyzeBtnDim}
76
+ disabled={!canAnalyze}
77
+ onClick={handleAnalyzeSelected}
78
+ >
79
+ {t('capture.analyzeSelected', { count: selectedSeqCount })}
80
+ </button>
81
+ )}
82
+
83
+ <div className={styles.spacer} />
84
+
85
+ <div className={styles.info}>
86
+ {selectedSeqCount > 0
87
+ ? `${t('data.selected') ?? '已选'} ${selectedSeqCount} / ${t('data.total') ?? '共'} ${totalCount} ${t('data.requests')}`
88
+ : `${t('data.total') ?? '共'} ${totalCount} ${t('data.requests')}`
89
+ }
90
+ </div>
91
+
92
+ <button
93
+ className={styles.analyzeBtnDim}
94
+ disabled={!(isStopped && hasRequests) || isAnalyzing}
95
+ onClick={onExport}
96
+ style={{ marginLeft: 8 }}
97
+ >
98
+ ⬇ {t('data.export')}
99
+ </button>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ export default AnalyzeBar
@@ -0,0 +1,67 @@
1
+ .panel {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--space-sm);
5
+ padding: var(--space-sm) var(--space-md);
6
+ background: var(--color-surface);
7
+ border-bottom: 1px solid var(--color-border);
8
+ position: relative;
9
+ }
10
+
11
+ .navBtns {
12
+ display: flex;
13
+ gap: 2px;
14
+ }
15
+
16
+ .addressBar {
17
+ flex: 1;
18
+ display: flex;
19
+ align-items: center;
20
+ background: var(--color-content);
21
+ border: 1px solid var(--color-border);
22
+ border-radius: var(--radius-input);
23
+ overflow: hidden;
24
+ transition: border-color var(--transition-fast);
25
+ }
26
+
27
+ .addressBar:focus-within {
28
+ border-color: var(--color-border-hover);
29
+ }
30
+
31
+ .addressInput {
32
+ flex: 1;
33
+ background: none;
34
+ border: none;
35
+ outline: none;
36
+ color: var(--text-primary);
37
+ font-size: var(--font-size-sm);
38
+ padding: 6px 10px;
39
+ font-family: var(--font-sans);
40
+ }
41
+
42
+ .addressInput::placeholder {
43
+ color: var(--text-disabled);
44
+ }
45
+
46
+ /* Capture controls (pill buttons) */
47
+ .captureControls {
48
+ display: flex;
49
+ gap: 4px;
50
+ flex-shrink: 0;
51
+ }
52
+
53
+ /* Loading progress bar */
54
+ .loadingBar {
55
+ position: absolute;
56
+ bottom: 0;
57
+ left: 0;
58
+ height: 2px;
59
+ background: var(--color-primary, #5865f2);
60
+ animation: loadingSlide 1.5s ease-in-out infinite;
61
+ }
62
+
63
+ @keyframes loadingSlide {
64
+ 0% { width: 0; left: 0; }
65
+ 50% { width: 60%; left: 20%; }
66
+ 100% { width: 0; left: 100%; }
67
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useState, useCallback } from 'react'
2
+ import { Button } from '../ui'
3
+ import { IconArrowLeft, IconArrowRight, IconReload, IconSend, IconDelete, IconCode } from '../ui/Icons'
4
+ import { useLocale } from '../i18n'
5
+ import styles from './BrowserPanel.module.css'
6
+
7
+ interface BrowserPanelProps {
8
+ currentUrl?: string
9
+ isLoading?: boolean
10
+ onNavigate: (url: string) => void
11
+ onBack: () => void
12
+ onForward: () => void
13
+ onReload: () => void
14
+ captureSlot?: React.ReactNode
15
+ onClearEnv?: () => void
16
+ onToggleDevTools?: () => void
17
+ }
18
+
19
+ const BrowserPanel: React.FC<BrowserPanelProps> = ({
20
+ currentUrl = '',
21
+ isLoading = false,
22
+ onNavigate,
23
+ onBack,
24
+ onForward,
25
+ onReload,
26
+ captureSlot,
27
+ onClearEnv,
28
+ onToggleDevTools,
29
+ }) => {
30
+ const [addressValue, setAddressValue] = useState(currentUrl)
31
+ const { t } = useLocale()
32
+
33
+ // Sync when currentUrl prop changes externally
34
+ React.useEffect(() => {
35
+ setAddressValue(currentUrl)
36
+ }, [currentUrl])
37
+
38
+ const handleNavigate = useCallback(() => {
39
+ const url = addressValue.trim()
40
+ if (!url) return
41
+
42
+ const finalUrl = /^https?:\/\//i.test(url) ? url : `https://${url}`
43
+ setAddressValue(finalUrl)
44
+ onNavigate(finalUrl)
45
+ }, [addressValue, onNavigate])
46
+
47
+ const handleKeyDown = useCallback(
48
+ (e: React.KeyboardEvent) => {
49
+ if (e.key === 'Enter') handleNavigate()
50
+ },
51
+ [handleNavigate]
52
+ )
53
+
54
+ return (
55
+ <div className={styles.panel}>
56
+ {/* Navigation buttons */}
57
+ <div className={styles.navBtns}>
58
+ <Button variant="ghost" size="sm" iconOnly icon={<IconArrowLeft size={14} />} onClick={onBack} title="Back" />
59
+ <Button variant="ghost" size="sm" iconOnly icon={<IconArrowRight size={14} />} onClick={onForward} title="Forward" />
60
+ <Button variant="ghost" size="sm" iconOnly icon={<IconReload size={14} />} onClick={onReload} title="Reload" />
61
+ {onToggleDevTools && (
62
+ <Button variant="ghost" size="sm" iconOnly icon={<IconCode size={14} />} title="DevTools" onClick={onToggleDevTools} />
63
+ )}
64
+ {onClearEnv && (
65
+ <Button variant="ghost" size="sm" iconOnly icon={<IconDelete size={14} />} title={t('data.clearEnv')} onClick={onClearEnv} />
66
+ )}
67
+ </div>
68
+
69
+ {/* Address bar */}
70
+ <div className={styles.addressBar}>
71
+ <input
72
+ className={styles.addressInput}
73
+ value={addressValue}
74
+ onChange={(e) => setAddressValue(e.target.value)}
75
+ onKeyDown={handleKeyDown}
76
+ placeholder="Enter URL..."
77
+ />
78
+ <Button variant="ghost" size="sm" iconOnly icon={<IconSend size={14} />} onClick={handleNavigate} title="Navigate" />
79
+ </div>
80
+
81
+ {/* Capture controls slot */}
82
+ {captureSlot && <div className={styles.captureControls}>{captureSlot}</div>}
83
+
84
+ {/* Loading progress bar */}
85
+ {isLoading && <div className={styles.loadingBar} />}
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export default BrowserPanel
@@ -0,0 +1,47 @@
1
+ .controlBar {
2
+ flex-shrink: 0;
3
+ background: var(--color-sidebar);
4
+ border-bottom: 1px solid var(--color-border);
5
+ }
6
+
7
+ .mainRow {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ padding: var(--space-sm) var(--space-md);
12
+ }
13
+
14
+ .leftGroup {
15
+ display: flex;
16
+ align-items: center;
17
+ gap: var(--space-sm);
18
+ }
19
+
20
+ .rightGroup {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: var(--space-sm);
24
+ }
25
+
26
+ .customRow {
27
+ padding: 0 var(--space-md) var(--space-sm);
28
+ display: flex;
29
+ gap: var(--space-sm);
30
+ align-items: flex-start;
31
+ }
32
+
33
+ .customInput {
34
+ flex: 1;
35
+ }
36
+
37
+ .customBtns {
38
+ display: flex;
39
+ gap: 4px;
40
+ margin-top: 2px;
41
+ }
42
+
43
+ .statusTag {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 4px;
47
+ }
@@ -0,0 +1,205 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { Button, Select, TextArea, Tag } from '../ui'
3
+ import {
4
+ IconPlay,
5
+ IconPause,
6
+ IconStop,
7
+ IconExperiment,
8
+ IconCheck,
9
+ IconClose,
10
+ IconLoading,
11
+ } from '../ui/Icons'
12
+ import { useLocale } from '../i18n'
13
+ import type { SessionStatus, PromptTemplate } from '../../shared/types'
14
+ import styles from './ControlBar.module.css'
15
+
16
+ interface ControlBarProps {
17
+ status: SessionStatus | null
18
+ onStart: () => void
19
+ onPause: () => void
20
+ onResume: () => void
21
+ onStop: () => void
22
+ onAnalyze: (purpose?: string) => void
23
+ hasRequests: boolean
24
+ isAnalyzing?: boolean
25
+ selectedSeqCount?: number
26
+ }
27
+
28
+ const ControlBar: React.FC<ControlBarProps> = ({
29
+ status,
30
+ onStart,
31
+ onPause,
32
+ onResume,
33
+ onStop,
34
+ onAnalyze,
35
+ hasRequests,
36
+ isAnalyzing = false,
37
+ selectedSeqCount = 0,
38
+ }) => {
39
+ const { t } = useLocale()
40
+ const [purposeId, setPurposeId] = useState<string>('auto')
41
+ const [customText, setCustomText] = useState('')
42
+ const [customExpanded, setCustomExpanded] = useState(false)
43
+ const [templates, setTemplates] = useState<PromptTemplate[]>([])
44
+
45
+ useEffect(() => {
46
+ window.electronAPI.getPromptTemplates().then(setTemplates).catch(() => {})
47
+ }, [])
48
+
49
+ const isRunning = status === 'running'
50
+ const isPaused = status === 'paused'
51
+ const isStopped = status === 'stopped' || status === null
52
+
53
+ const handlePurposeChange = (value: string) => {
54
+ if (value === 'custom') {
55
+ setCustomExpanded(true)
56
+ } else {
57
+ setPurposeId(value)
58
+ setCustomExpanded(false)
59
+ }
60
+ }
61
+
62
+ const handleCustomConfirm = () => {
63
+ const trimmed = customText.trim()
64
+ if (trimmed) {
65
+ setPurposeId('custom')
66
+ setCustomExpanded(false)
67
+ }
68
+ }
69
+
70
+ const handleCustomCancel = () => {
71
+ setCustomExpanded(false)
72
+ if (purposeId !== 'custom') setCustomText('')
73
+ }
74
+
75
+ const handleAnalyze = () => {
76
+ if (purposeId === 'custom') {
77
+ onAnalyze(customText.trim() || undefined)
78
+ } else if (purposeId === 'auto') {
79
+ onAnalyze(undefined)
80
+ } else {
81
+ onAnalyze(purposeId)
82
+ }
83
+ }
84
+
85
+ const selectOptions = [
86
+ ...templates.map((tpl) => ({ label: tpl.name, value: tpl.id })),
87
+ { label: t('capture.custom'), value: 'custom' },
88
+ ]
89
+
90
+ return (
91
+ <div className={styles.controlBar}>
92
+ {/* Main control row */}
93
+ <div className={styles.mainRow}>
94
+ <div className={styles.leftGroup}>
95
+ <Button
96
+ variant="success"
97
+ icon={<IconPlay size={14} />}
98
+ disabled={!isStopped}
99
+ onClick={onStart}
100
+ >
101
+ {t('capture.start')}
102
+ </Button>
103
+
104
+ <Button
105
+ variant="default"
106
+ icon={isPaused ? <IconPlay size={14} /> : <IconPause size={14} />}
107
+ disabled={!(isRunning || isPaused)}
108
+ onClick={isPaused ? onResume : onPause}
109
+ >
110
+ {isPaused ? t('capture.resume') : t('capture.pause')}
111
+ </Button>
112
+
113
+ <Button
114
+ variant="danger"
115
+ icon={<IconStop size={14} />}
116
+ disabled={!(isRunning || isPaused)}
117
+ onClick={onStop}
118
+ >
119
+ {t('capture.stop')}
120
+ </Button>
121
+
122
+ <Select
123
+ value={customExpanded ? 'custom' : purposeId}
124
+ onChange={handlePurposeChange}
125
+ style={{ width: 160 }}
126
+ disabled={isAnalyzing}
127
+ options={selectOptions}
128
+ />
129
+
130
+ <Button
131
+ variant="primary"
132
+ icon={<IconExperiment size={14} />}
133
+ disabled={!(isStopped && hasRequests) || isAnalyzing}
134
+ loading={isAnalyzing}
135
+ onClick={handleAnalyze}
136
+ >
137
+ {isAnalyzing
138
+ ? t('capture.analyze') + '...'
139
+ : selectedSeqCount > 0
140
+ ? t('capture.analyzeSelected', { count: selectedSeqCount })
141
+ : t('capture.analyze')}
142
+ </Button>
143
+ </div>
144
+
145
+ {/* Right side: status tags */}
146
+ <div className={styles.rightGroup}>
147
+ {purposeId === 'custom' && customText.trim() && !customExpanded && (
148
+ <Tag color="info" style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
149
+ {customText.trim()}
150
+ </Tag>
151
+ )}
152
+ {isRunning && (
153
+ <Tag color="success">
154
+ <span className={styles.statusTag}>
155
+ <IconLoading size={12} />
156
+ {t('capture.running')}
157
+ </span>
158
+ </Tag>
159
+ )}
160
+ {isPaused && <Tag color="warning">{t('capture.paused')}</Tag>}
161
+ {isStopped && status !== null && <Tag color="default">{t('capture.stopped')}</Tag>}
162
+ </div>
163
+ </div>
164
+
165
+ {/* Inline custom purpose input */}
166
+ {customExpanded && (
167
+ <div className={styles.customRow}>
168
+ <div className={styles.customInput}>
169
+ <TextArea
170
+ value={customText}
171
+ onChange={(e) => setCustomText(e.target.value)}
172
+ placeholder={t('capture.customPurpose')}
173
+ autoSize={{ minRows: 1, maxRows: 4 }}
174
+ autoFocus
175
+ onKeyDown={(e) => {
176
+ if (e.key === 'Enter' && !e.shiftKey) {
177
+ e.preventDefault()
178
+ handleCustomConfirm()
179
+ }
180
+ }}
181
+ />
182
+ </div>
183
+ <div className={styles.customBtns}>
184
+ <Button
185
+ variant="primary"
186
+ size="sm"
187
+ iconOnly
188
+ icon={<IconCheck size={14} />}
189
+ disabled={!customText.trim()}
190
+ onClick={handleCustomConfirm}
191
+ />
192
+ <Button
193
+ size="sm"
194
+ iconOnly
195
+ icon={<IconClose size={14} />}
196
+ onClick={handleCustomCancel}
197
+ />
198
+ </div>
199
+ </div>
200
+ )}
201
+ </div>
202
+ )
203
+ }
204
+
205
+ export default ControlBar