@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,429 @@
1
+ import React, { useEffect, useRef, useState } from 'react'
2
+ import { Button, Tag, Empty, Spinner } from '../ui'
3
+ import { IconRobot, IconFileText } from '../ui/Icons'
4
+ import { useLocale } from '../i18n'
5
+ import ReactMarkdown from 'react-markdown'
6
+ import remarkGfm from 'remark-gfm'
7
+ import rehypeHighlight from 'rehype-highlight'
8
+ import type { AnalysisReport, ChatMessage, CapturedRequest, JsHookRecord } from '@shared/types'
9
+ import { stripToolContext } from '@shared/types'
10
+ import { AiLogView } from './AiLogView'
11
+ import styles from './ReportView.module.css'
12
+
13
+ interface ReportViewProps {
14
+ report: AnalysisReport | null
15
+ isAnalyzing: boolean
16
+ analysisError: string | null
17
+ streamingContent: string
18
+ onReAnalyze: (purpose?: string) => void
19
+ onCancelAnalysis: () => void
20
+ chatHistory: ChatMessage[]
21
+ isChatting: boolean
22
+ chatError: string | null
23
+ onSendFollowUp: (message: string) => void
24
+ // Context panel data
25
+ sessionName?: string
26
+ requests?: CapturedRequest[]
27
+ hooks?: JsHookRecord[]
28
+ }
29
+
30
+ function formatTokens(tokens: number | null): string {
31
+ if (tokens === null) return '--'
32
+ return tokens.toLocaleString()
33
+ }
34
+
35
+ // Streaming text display with cursor blinking effect
36
+ const StreamingDisplay: React.FC<{ content: string }> = ({ content }) => {
37
+ const containerRef = useRef<HTMLDivElement>(null)
38
+
39
+ useEffect(() => {
40
+ if (containerRef.current) containerRef.current.scrollTop = containerRef.current.scrollHeight
41
+ }, [content])
42
+
43
+ return (
44
+ <div ref={containerRef} className={styles.streamingContainer}>
45
+ <div className="report-markdown-content">
46
+ <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
47
+ {content}
48
+ </ReactMarkdown>
49
+ <span className={styles.cursor} />
50
+ </div>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ // Quick follow-up suggestions
56
+ const QUICK_QUESTION_KEYS = [
57
+ 'report.genPython',
58
+ 'report.explainCrypto',
59
+ 'report.securityRisks',
60
+ 'report.listApiParams',
61
+ ] as const
62
+
63
+ // Extract unique API endpoints from requests
64
+ function extractEndpoints(requests: CapturedRequest[]): { method: string; path: string }[] {
65
+ const seen = new Set<string>()
66
+ const endpoints: { method: string; path: string }[] = []
67
+ for (const r of requests) {
68
+ try {
69
+ const url = new URL(r.url)
70
+ const key = `${r.method} ${url.pathname}`
71
+ if (!seen.has(key)) {
72
+ seen.add(key)
73
+ endpoints.push({ method: r.method, path: url.pathname })
74
+ }
75
+ } catch {
76
+ // skip invalid URLs
77
+ }
78
+ }
79
+ return endpoints.slice(0, 8) // limit to top 8
80
+ }
81
+
82
+ function getMethodColor(method: string): string {
83
+ switch (method.toUpperCase()) {
84
+ case 'GET': return 'var(--color-success)'
85
+ case 'POST': return 'var(--color-info)'
86
+ case 'PUT': return 'var(--color-orange)'
87
+ case 'DELETE': return 'var(--color-error)'
88
+ default: return 'var(--text-muted)'
89
+ }
90
+ }
91
+
92
+ // Summarize hook types
93
+ function summarizeHooks(hooks: JsHookRecord[]): { type: string; count: number; color: string }[] {
94
+ const counts: Record<string, number> = {}
95
+ for (const h of hooks) {
96
+ const type = h.hook_type || 'unknown'
97
+ counts[type] = (counts[type] || 0) + 1
98
+ }
99
+ const colorMap: Record<string, string> = {
100
+ crypto: 'var(--color-warning)',
101
+ fetch: 'var(--color-info)',
102
+ xhr: 'var(--color-info)',
103
+ cookie: 'var(--color-error)',
104
+ }
105
+ return Object.entries(counts).map(([type, count]) => ({
106
+ type,
107
+ count,
108
+ color: colorMap[type] || 'var(--text-muted)',
109
+ }))
110
+ }
111
+
112
+ const ReportView: React.FC<ReportViewProps> = ({
113
+ report,
114
+ isAnalyzing,
115
+ analysisError,
116
+ streamingContent,
117
+ onReAnalyze,
118
+ onCancelAnalysis,
119
+ chatHistory,
120
+ isChatting,
121
+ chatError,
122
+ onSendFollowUp,
123
+ sessionName,
124
+ requests = [],
125
+ hooks = [],
126
+ }) => {
127
+ const { t } = useLocale()
128
+ const [chatInput, setChatInput] = useState('')
129
+ const [showAiLog, setShowAiLog] = useState(false)
130
+ const reportBodyRef = useRef<HTMLDivElement>(null)
131
+
132
+ // Auto-scroll report body when streaming or new chat messages arrive
133
+ useEffect(() => {
134
+ if ((streamingContent || isChatting) && reportBodyRef.current) {
135
+ reportBodyRef.current.scrollTop = reportBodyRef.current.scrollHeight
136
+ }
137
+ }, [streamingContent, isChatting])
138
+
139
+ useEffect(() => {
140
+ if (chatHistory.length > 2 && reportBodyRef.current) {
141
+ requestAnimationFrame(() => {
142
+ if (reportBodyRef.current) {
143
+ reportBodyRef.current.scrollTop = reportBodyRef.current.scrollHeight
144
+ }
145
+ })
146
+ }
147
+ }, [chatHistory.length])
148
+
149
+ const handleSend = () => {
150
+ const trimmed = chatInput.trim()
151
+ if (!trimmed || isChatting) return
152
+ onSendFollowUp(trimmed)
153
+ setChatInput('')
154
+ }
155
+
156
+ const handleExport = async () => {
157
+ if (!report) return
158
+ const defaultName = `report-${new Date(report.created_at).toISOString().slice(0, 10)}-${report.llm_model}.md`
159
+ let content = report.report_content
160
+ const followUps = chatHistory.slice(2)
161
+ if (followUps.length > 0) {
162
+ content += '\n\n---\n\n## Follow-up Chat\n'
163
+ for (const msg of followUps) {
164
+ const label = msg.role === 'user' ? '**User**' : '**AI**'
165
+ content += `\n${label}:\n\n${stripToolContext(msg.content)}\n`
166
+ }
167
+ }
168
+ await window.electronAPI.exportFile(defaultName, content)
169
+ }
170
+
171
+ const endpoints = extractEndpoints(requests)
172
+ const hookSummary = summarizeHooks(hooks)
173
+
174
+ // Render right context panel
175
+ const renderContextPanel = () => (
176
+ <div className={styles.reportContext}>
177
+ <div className={styles.contextHeader}>{t('report.title')}</div>
178
+
179
+ {/* Session info */}
180
+ <div className={styles.contextSection}>
181
+ <div className={styles.contextLabel}>{t('status.session')}</div>
182
+ {sessionName && (
183
+ <div className={styles.contextItem}>
184
+ <div className={styles.contextDot} style={{ background: 'var(--color-purple)' }} />
185
+ {sessionName}
186
+ </div>
187
+ )}
188
+ <div className={styles.contextItem}>
189
+ <div className={styles.contextDot} style={{ background: 'var(--color-success)' }} />
190
+ {requests.length} {t('data.requests')} · {hooks.length} {t('data.hooks')}
191
+ </div>
192
+ </div>
193
+
194
+ {/* Key endpoints */}
195
+ {endpoints.length > 0 && (
196
+ <div className={styles.contextSection}>
197
+ <div className={styles.contextLabel}>Endpoints</div>
198
+ {endpoints.map((ep, i) => (
199
+ <div key={i} className={styles.contextEndpoint}>
200
+ <span className={styles.contextMethod} style={{ color: getMethodColor(ep.method) }}>
201
+ {ep.method}
202
+ </span>
203
+ <span className={styles.contextPath}>{ep.path}</span>
204
+ </div>
205
+ ))}
206
+ </div>
207
+ )}
208
+
209
+ {/* Detected hooks */}
210
+ {hookSummary.length > 0 && (
211
+ <div className={styles.contextSection}>
212
+ <div className={styles.contextLabel}>{t('data.hooks')}</div>
213
+ {hookSummary.map((h, i) => (
214
+ <div key={i} className={styles.contextItem}>
215
+ <div className={styles.contextDot} style={{ background: h.color }} />
216
+ {h.type} × {h.count}
217
+ </div>
218
+ ))}
219
+ </div>
220
+ )}
221
+
222
+ {/* Report metadata if available */}
223
+ {report && (
224
+ <div className={styles.contextSection}>
225
+ <div className={styles.contextLabel}>LLM</div>
226
+ <div className={styles.contextItem}>
227
+ <div className={styles.contextDot} style={{ background: 'var(--color-info)' }} />
228
+ {report.llm_model}
229
+ </div>
230
+ {report.prompt_tokens != null && report.completion_tokens != null && (
231
+ <div className={styles.contextItem}>
232
+ <div className={styles.contextDot} style={{ background: 'var(--color-success)' }} />
233
+ {formatTokens(report.prompt_tokens + report.completion_tokens)} tokens
234
+ </div>
235
+ )}
236
+ </div>
237
+ )}
238
+ </div>
239
+ )
240
+
241
+ // Analyzing state — full width, no context panel
242
+ if (isAnalyzing) {
243
+ return (
244
+ <div className={styles.reportContainer}>
245
+ <div className={styles.reportMain}>
246
+ <div className={styles.reportBody} ref={reportBodyRef}>
247
+ <div className={styles.reportScroll}>
248
+ <div className={styles.analyzingHeader}>
249
+ <Spinner size="sm" />
250
+ <span>
251
+ <IconRobot size={14} style={{ marginRight: 4 }} />
252
+ {t('report.analyzing')}
253
+ </span>
254
+ <div style={{ flex: 1 }} />
255
+ <Button size="sm" onClick={onCancelAnalysis}>
256
+ {t('report.stopAnalysis')}
257
+ </Button>
258
+ </div>
259
+ {streamingContent ? (
260
+ <StreamingDisplay content={streamingContent} />
261
+ ) : (
262
+ <div className={styles.preparingState}>
263
+ <Spinner />
264
+ <div style={{ marginTop: 12 }}>{t('report.preparing')}</div>
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ </div>
270
+ {renderContextPanel()}
271
+ </div>
272
+ )
273
+ }
274
+
275
+ // No report yet
276
+ if (!report) {
277
+ return (
278
+ <div className={styles.reportContainer}>
279
+ <div className={styles.reportMain}>
280
+ <div className={styles.emptyState}>
281
+ {analysisError && (
282
+ <div className={styles.errorAlert}>
283
+ <div className={styles.errorTitle}>{t('report.analysisFailed')}</div>
284
+ <div className={styles.errorDesc}>{analysisError}</div>
285
+ </div>
286
+ )}
287
+ <Empty
288
+ icon={<IconFileText size={48} style={{ opacity: 0.25 }} />}
289
+ description={t('report.noReport')}
290
+ />
291
+ <Button variant="primary" icon={<IconRobot size={14} />} onClick={() => onReAnalyze()}>
292
+ {t('report.startAnalysis')}
293
+ </Button>
294
+ </div>
295
+ </div>
296
+ {renderContextPanel()}
297
+ </div>
298
+ )
299
+ }
300
+
301
+ // Has report — show AI Log view if toggled
302
+ if (showAiLog) {
303
+ return (
304
+ <AiLogView
305
+ sessionId={report.session_id}
306
+ sessionName={sessionName}
307
+ onBack={() => setShowAiLog(false)}
308
+ />
309
+ )
310
+ }
311
+
312
+ // Has report — full layout with toolbar + content + chat + context panel
313
+ return (
314
+ <div className={styles.reportContainer}>
315
+ <div className={styles.reportMain}>
316
+ {/* Toolbar */}
317
+ <div className={styles.reportToolbar}>
318
+ <div className={styles.toolLabel}>{t('capture.autoDetect')}</div>
319
+ <button className={styles.toolBtnPrimary}>✦ {report.llm_model}</button>
320
+ <div className={styles.toolSpacer} />
321
+ <button className={styles.toolBtn} onClick={handleExport}>⬇ {t('report.export')}</button>
322
+ <button className={styles.toolBtn} onClick={() => onReAnalyze()}>↻ {t('report.reanalyze')}</button>
323
+ <button className={styles.toolBtn} onClick={() => setShowAiLog(true)}>📋 {t('aiLog.title')}</button>
324
+ </div>
325
+
326
+ {/* Report content */}
327
+ <div className={styles.reportBody} ref={reportBodyRef}>
328
+ <div className={styles.reportScroll}>
329
+ {/* Metadata row */}
330
+ <div className={styles.metaRow}>
331
+ <span>✦ {report.llm_model}</span>
332
+ <span>◷ {new Date(report.created_at).toLocaleString()}</span>
333
+ <span>{requests.length} {t('data.requests')}</span>
334
+ </div>
335
+
336
+ {/* Markdown content */}
337
+ <div className="report-markdown-content" style={{ overflowWrap: 'break-word', wordBreak: 'break-word' }}>
338
+ <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
339
+ {report.report_content}
340
+ </ReactMarkdown>
341
+ </div>
342
+
343
+ {/* Chat history */}
344
+ {chatHistory.slice(2).map((msg, i) => (
345
+ <div key={i} className={`${styles.chatMsg} ${msg.role === 'user' ? styles.chatMsgUser : styles.chatMsgAi}`}>
346
+ <Tag color={msg.role === 'user' ? 'info' : 'success'} style={{ marginBottom: 4 }}>
347
+ {msg.role === 'user' ? 'You' : 'AI'}
348
+ </Tag>
349
+ <div className="report-markdown-content">
350
+ <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeHighlight]}>
351
+ {stripToolContext(msg.content)}
352
+ </ReactMarkdown>
353
+ </div>
354
+ </div>
355
+ ))}
356
+
357
+ {/* Streaming follow-up */}
358
+ {isChatting && streamingContent && (
359
+ <div className={`${styles.chatMsg} ${styles.chatMsgAi}`}>
360
+ <Tag color="success" style={{ marginBottom: 4 }}>AI</Tag>
361
+ <StreamingDisplay content={streamingContent} />
362
+ </div>
363
+ )}
364
+
365
+ {isChatting && !streamingContent && (
366
+ <div style={{ textAlign: 'center', padding: 12 }}>
367
+ <Spinner size="sm" />
368
+ <span style={{ marginLeft: 8, color: 'var(--text-muted)' }}>{t('report.thinking')}</span>
369
+ </div>
370
+ )}
371
+
372
+ {chatError && (
373
+ <div className={styles.errorAlert} style={{ marginTop: 12 }}>
374
+ <div className={styles.errorTitle}>{t('report.followUpFailed')}</div>
375
+ <div className={styles.errorDesc}>{chatError}</div>
376
+ </div>
377
+ )}
378
+ </div>
379
+ </div>
380
+
381
+ {/* Chat section */}
382
+ <div className={styles.chatSection}>
383
+ <div className={styles.chatSuggestions}>
384
+ {QUICK_QUESTION_KEYS.map((key, i) => {
385
+ const text = t(key)
386
+ return (
387
+ <button
388
+ key={i}
389
+ className={styles.chatChip}
390
+ disabled={isChatting}
391
+ onClick={() => { if (!isChatting) onSendFollowUp(text) }}
392
+ >
393
+ {text}
394
+ </button>
395
+ )
396
+ })}
397
+ </div>
398
+ <div className={styles.chatInputBar}>
399
+ <input
400
+ className={styles.chatInput}
401
+ value={chatInput}
402
+ onChange={(e) => setChatInput(e.target.value)}
403
+ placeholder={t('report.askFollowUp')}
404
+ disabled={isChatting}
405
+ onKeyDown={(e) => {
406
+ if (e.key === 'Enter' && !e.shiftKey) {
407
+ e.preventDefault()
408
+ handleSend()
409
+ }
410
+ }}
411
+ />
412
+ <button
413
+ className={styles.chatSend}
414
+ onClick={handleSend}
415
+ disabled={isChatting || !chatInput.trim()}
416
+ >
417
+
418
+ </button>
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ {/* Right context panel */}
424
+ {renderContextPanel()}
425
+ </div>
426
+ )
427
+ }
428
+
429
+ export default ReportView
@@ -0,0 +1,191 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ overflow: hidden;
6
+ }
7
+
8
+ /* Header: METHOD STATUS · TIME */
9
+ .detailHeader {
10
+ padding: 12px 16px;
11
+ border-bottom: 1px solid var(--color-border);
12
+ flex-shrink: 0;
13
+ }
14
+
15
+ .detailTitle {
16
+ font-size: var(--font-size-2xs);
17
+ color: var(--text-primary);
18
+ font-weight: 500;
19
+ margin-bottom: 4px;
20
+ }
21
+
22
+ .detailUrl {
23
+ font-size: var(--font-size-3xs);
24
+ color: var(--text-muted);
25
+ word-break: break-all;
26
+ }
27
+
28
+ /* Simple text tabs */
29
+ .detailTabs {
30
+ height: 32px;
31
+ border-bottom: 1px solid var(--color-border);
32
+ display: flex;
33
+ padding: 0 16px;
34
+ gap: 20px;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .detailTab {
39
+ font-size: var(--font-size-3xs);
40
+ color: var(--text-muted);
41
+ display: flex;
42
+ align-items: center;
43
+ position: relative;
44
+ cursor: pointer;
45
+ background: none;
46
+ border: none;
47
+ border-bottom: 1px solid transparent;
48
+ font-family: var(--font-sans);
49
+ padding: 0;
50
+ transition: color var(--transition-fast);
51
+ }
52
+
53
+ .detailTab:hover {
54
+ color: var(--text-secondary);
55
+ }
56
+
57
+ .detailTabActive {
58
+ color: var(--text-secondary);
59
+ border-bottom-color: var(--text-secondary);
60
+ }
61
+
62
+ /* Tab body — scrollable */
63
+ .detailBody {
64
+ flex: 1;
65
+ padding: 12px 16px;
66
+ overflow: auto;
67
+ min-height: 0;
68
+ }
69
+
70
+ /* Section labels (REQUEST HEADERS, RESPONSE HEADERS) */
71
+ .sectionLabel {
72
+ font-size: var(--font-size-3xs);
73
+ color: var(--text-label);
74
+ letter-spacing: 1px;
75
+ text-transform: uppercase;
76
+ margin-bottom: 8px;
77
+ }
78
+
79
+ .kvSection {
80
+ padding: 0;
81
+ }
82
+
83
+ /* KV row */
84
+ .kvRow {
85
+ display: flex;
86
+ padding: 4px 0;
87
+ font-size: var(--font-size-3xs);
88
+ border-bottom: 1px solid var(--color-border-subtle);
89
+ }
90
+
91
+ .kvKey {
92
+ width: 140px;
93
+ color: var(--text-muted);
94
+ flex-shrink: 0;
95
+ }
96
+
97
+ .kvVal {
98
+ color: var(--text-secondary);
99
+ word-break: break-all;
100
+ }
101
+
102
+ .emptyHint {
103
+ font-size: var(--font-size-3xs);
104
+ color: var(--text-disabled);
105
+ padding: 4px 0;
106
+ }
107
+
108
+ /* Code block for body/response */
109
+ .codeBlock {
110
+ background: var(--color-surface);
111
+ border: 1px solid var(--color-border-subtle);
112
+ border-radius: var(--radius-button);
113
+ padding: 10px 14px;
114
+ margin: 0;
115
+ font-family: var(--font-mono);
116
+ font-size: var(--font-size-2xs);
117
+ color: var(--text-secondary);
118
+ line-height: 1.6;
119
+ white-space: pre-wrap;
120
+ word-break: break-all;
121
+ overflow: auto;
122
+ max-height: 400px;
123
+ }
124
+
125
+ /* Key-value metadata grid */
126
+ .metaGrid {
127
+ display: grid;
128
+ grid-template-columns: auto 1fr;
129
+ gap: 4px 16px;
130
+ margin-bottom: var(--space-lg);
131
+ font-size: var(--font-size-sm);
132
+ }
133
+
134
+ .metaLabel {
135
+ color: var(--text-muted);
136
+ font-weight: 500;
137
+ }
138
+
139
+ .metaValue {
140
+ color: var(--text-secondary);
141
+ font-family: var(--font-mono);
142
+ font-size: var(--font-size-xs);
143
+ }
144
+
145
+ /* Hook list */
146
+ .hookList {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: var(--space-sm);
150
+ }
151
+
152
+ .hookItem {
153
+ padding: var(--space-sm) var(--space-md);
154
+ background: var(--color-surface);
155
+ border-radius: var(--radius-button);
156
+ border: 1px solid var(--color-border-subtle);
157
+ }
158
+
159
+ .hookHeader {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: var(--space-sm);
163
+ margin-bottom: 4px;
164
+ }
165
+
166
+ .hookFn {
167
+ font-family: var(--font-mono);
168
+ font-size: var(--font-size-xs);
169
+ background: var(--color-content);
170
+ padding: 1px 4px;
171
+ border-radius: 3px;
172
+ color: var(--color-warning);
173
+ }
174
+
175
+ .hookTime {
176
+ margin-left: auto;
177
+ font-size: var(--font-size-2xs);
178
+ color: var(--text-muted);
179
+ white-space: nowrap;
180
+ }
181
+
182
+ .hookArgs {
183
+ font-family: var(--font-mono);
184
+ font-size: var(--font-size-xs);
185
+ color: var(--text-muted);
186
+ overflow: hidden;
187
+ text-overflow: ellipsis;
188
+ display: -webkit-box;
189
+ -webkit-line-clamp: 2;
190
+ -webkit-box-orient: vertical;
191
+ }