@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,383 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CapturedRequest,
|
|
4
|
+
JsHookRecord,
|
|
5
|
+
StorageSnapshot,
|
|
6
|
+
AnalysisReport,
|
|
7
|
+
ChatMessage,
|
|
8
|
+
InteractionEvent,
|
|
9
|
+
} from "@shared/types";
|
|
10
|
+
import { IPC_CHANNELS } from "@shared/types";
|
|
11
|
+
|
|
12
|
+
interface UseCaptureState {
|
|
13
|
+
requests: CapturedRequest[];
|
|
14
|
+
hooks: JsHookRecord[];
|
|
15
|
+
snapshots: StorageSnapshot[];
|
|
16
|
+
reports: AnalysisReport[];
|
|
17
|
+
interactions: InteractionEvent[];
|
|
18
|
+
isAnalyzing: boolean;
|
|
19
|
+
analysisError: string | null;
|
|
20
|
+
streamingContent: string;
|
|
21
|
+
selectedRequest: CapturedRequest | null;
|
|
22
|
+
chatHistory: ChatMessage[];
|
|
23
|
+
isChatting: boolean;
|
|
24
|
+
chatError: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface UseCaptureReturn extends UseCaptureState {
|
|
28
|
+
loadData: (sessionId: string) => Promise<void>;
|
|
29
|
+
clearData: () => void;
|
|
30
|
+
clearCaptureData: (sessionId: string) => Promise<void>;
|
|
31
|
+
selectRequest: (request: CapturedRequest | null) => void;
|
|
32
|
+
startAnalysis: (sessionId: string, purpose?: string, selectedSeqs?: number[]) => Promise<void>;
|
|
33
|
+
cancelAnalysis: (sessionId: string) => Promise<void>;
|
|
34
|
+
sendFollowUp: (sessionId: string, message: string) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const INITIAL_STATE: UseCaptureState = {
|
|
38
|
+
requests: [],
|
|
39
|
+
hooks: [],
|
|
40
|
+
snapshots: [],
|
|
41
|
+
reports: [],
|
|
42
|
+
interactions: [],
|
|
43
|
+
isAnalyzing: false,
|
|
44
|
+
analysisError: null,
|
|
45
|
+
streamingContent: "",
|
|
46
|
+
selectedRequest: null,
|
|
47
|
+
chatHistory: [],
|
|
48
|
+
isChatting: false,
|
|
49
|
+
chatError: null,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function useCapture(sessionId: string | null): UseCaptureReturn {
|
|
53
|
+
const [state, setState] = useState<UseCaptureState>(INITIAL_STATE);
|
|
54
|
+
const sessionIdRef = useRef(sessionId);
|
|
55
|
+
|
|
56
|
+
// Keep ref in sync for use in callbacks
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
sessionIdRef.current = sessionId;
|
|
59
|
+
}, [sessionId]);
|
|
60
|
+
|
|
61
|
+
// Clear all data
|
|
62
|
+
const clearData = useCallback(() => {
|
|
63
|
+
setState(INITIAL_STATE);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
// Clear all capture data from DB and reset local state
|
|
67
|
+
const clearCaptureData = useCallback(async (sid: string) => {
|
|
68
|
+
await window.electronAPI.clearCaptureData(sid);
|
|
69
|
+
setState(INITIAL_STATE);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
// Select a request for detail view
|
|
73
|
+
const selectRequest = useCallback((request: CapturedRequest | null) => {
|
|
74
|
+
setState((prev) => ({ ...prev, selectedRequest: request }));
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
// Load all data for a session from main process
|
|
78
|
+
const loadData = useCallback(async (sid: string) => {
|
|
79
|
+
try {
|
|
80
|
+
const [requests, hooks, snapshots, reports, interactions] = await Promise.all([
|
|
81
|
+
window.electronAPI.getRequests(sid),
|
|
82
|
+
window.electronAPI.getHooks(sid),
|
|
83
|
+
window.electronAPI.getStorage(sid),
|
|
84
|
+
window.electronAPI.getReports(sid),
|
|
85
|
+
window.electronAPI.getInteractions(sid),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
// Restore chat history for the latest report
|
|
89
|
+
let chatHistory: ChatMessage[] = [];
|
|
90
|
+
if (reports.length > 0) {
|
|
91
|
+
const latestReport = reports.sort((a, b) => b.created_at - a.created_at)[0];
|
|
92
|
+
const savedMessages = await window.electronAPI.getChatMessages(latestReport.id);
|
|
93
|
+
if (savedMessages.length > 0) {
|
|
94
|
+
chatHistory = savedMessages as ChatMessage[];
|
|
95
|
+
} else {
|
|
96
|
+
// Legacy report without persisted chat — reconstruct [system, assistant] prefix
|
|
97
|
+
// so that chatHistory.slice(2) renders follow-up messages correctly
|
|
98
|
+
const reqSummary = requests.slice(0, 50).map(r => {
|
|
99
|
+
let path = r.url;
|
|
100
|
+
try { path = new URL(r.url).pathname; } catch { /* keep full url */ }
|
|
101
|
+
return `#${r.sequence} ${r.method} ${path} → ${r.status_code ?? '?'}`;
|
|
102
|
+
}).join('\n');
|
|
103
|
+
|
|
104
|
+
const hookSummary = hooks.length > 0
|
|
105
|
+
? '\n\nDetected hooks:\n' + hooks.slice(0, 20).map(h =>
|
|
106
|
+
`[${h.hook_type}] ${h.function_name}`
|
|
107
|
+
).join('\n')
|
|
108
|
+
: '';
|
|
109
|
+
|
|
110
|
+
const contextBlock = reqSummary
|
|
111
|
+
? `\n\n<captured_data_summary>\nCaptured ${requests.length} requests:\n${reqSummary}${requests.length > 50 ? `\n... and ${requests.length - 50} more` : ''}${hookSummary}\n</captured_data_summary>`
|
|
112
|
+
: '';
|
|
113
|
+
|
|
114
|
+
const systemContent = `你是一位网站协议分析专家。基于之前的分析报告和捕获数据,回答用户的追问。保持技术精确,用中文回复。\n\n你可以使用 get_request_detail 工具,通过传入请求序号(seq)来查看任意请求的完整详情(请求头、请求体、响应头、响应体)。当用户追问某个具体请求或需要更多细节时,请主动调用此工具获取数据。${contextBlock}`;
|
|
115
|
+
|
|
116
|
+
chatHistory = [
|
|
117
|
+
{ role: 'system' as const, content: systemContent },
|
|
118
|
+
{ role: 'assistant' as const, content: latestReport.report_content },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// Persist for future loads
|
|
122
|
+
window.electronAPI.saveChatMessages(latestReport.id, chatHistory)
|
|
123
|
+
.catch(err => console.error("Failed to backfill chat messages:", err));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Only update if session hasn't changed while loading
|
|
128
|
+
if (sessionIdRef.current === sid) {
|
|
129
|
+
setState((prev) => ({
|
|
130
|
+
...prev,
|
|
131
|
+
requests: requests.sort((a, b) => a.sequence - b.sequence),
|
|
132
|
+
hooks: hooks.sort((a, b) => b.timestamp - a.timestamp),
|
|
133
|
+
snapshots,
|
|
134
|
+
reports: reports.sort((a, b) => b.created_at - a.created_at),
|
|
135
|
+
interactions: (interactions || []).sort((a, b) => a.sequence - b.sequence),
|
|
136
|
+
chatHistory,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error("Failed to load capture data:", err);
|
|
141
|
+
}
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
// Start AI analysis for a session
|
|
145
|
+
const startAnalysis = useCallback(async (sid: string, purpose?: string, selectedSeqs?: number[]) => {
|
|
146
|
+
setState((prev) => ({
|
|
147
|
+
...prev,
|
|
148
|
+
isAnalyzing: true,
|
|
149
|
+
analysisError: null,
|
|
150
|
+
streamingContent: "",
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const report = await window.electronAPI.startAnalysis(sid, purpose, selectedSeqs);
|
|
155
|
+
|
|
156
|
+
// Only update if session hasn't changed
|
|
157
|
+
if (sessionIdRef.current === sid) {
|
|
158
|
+
// Build context summary from captured data for follow-up chat
|
|
159
|
+
// (read current state synchronously via a mini-setState that returns prev unchanged)
|
|
160
|
+
let systemContent = '';
|
|
161
|
+
setState((prev) => {
|
|
162
|
+
const reqSummary = prev.requests.slice(0, 50).map(r => {
|
|
163
|
+
let path = r.url
|
|
164
|
+
try { path = new URL(r.url).pathname } catch { /* keep full url */ }
|
|
165
|
+
return `#${r.sequence} ${r.method} ${path} → ${r.status_code ?? '?'}`
|
|
166
|
+
}).join('\n')
|
|
167
|
+
|
|
168
|
+
const hookSummary = prev.hooks.length > 0
|
|
169
|
+
? '\n\nDetected hooks:\n' + prev.hooks.slice(0, 20).map(h =>
|
|
170
|
+
`[${h.hook_type}] ${h.function_name}`
|
|
171
|
+
).join('\n')
|
|
172
|
+
: ''
|
|
173
|
+
|
|
174
|
+
const contextBlock = reqSummary
|
|
175
|
+
? `\n\n<captured_data_summary>\nCaptured ${prev.requests.length} requests:\n${reqSummary}${prev.requests.length > 50 ? `\n... and ${prev.requests.length - 50} more` : ''}${hookSummary}\n</captured_data_summary>`
|
|
176
|
+
: ''
|
|
177
|
+
|
|
178
|
+
systemContent = `你是一位网站协议分析专家。基于之前的分析报告和捕获数据,回答用户的追问。保持技术精确,用中文回复。
|
|
179
|
+
|
|
180
|
+
你可以使用 get_request_detail 工具,通过传入请求序号(seq)来查看任意请求的完整详情(请求头、请求体、响应头、响应体)。当用户追问某个具体请求或需要更多细节时,请主动调用此工具获取数据。${contextBlock}`
|
|
181
|
+
|
|
182
|
+
const chatHistory: ChatMessage[] = [
|
|
183
|
+
{ role: 'system' as const, content: systemContent },
|
|
184
|
+
{ role: 'assistant' as const, content: report.report_content },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
...prev,
|
|
189
|
+
isAnalyzing: false,
|
|
190
|
+
streamingContent: "",
|
|
191
|
+
reports: [report, ...prev.reports],
|
|
192
|
+
chatHistory,
|
|
193
|
+
chatError: null,
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Persist initial chat messages (system prompt + report) to database
|
|
198
|
+
window.electronAPI.saveChatMessages(report.id, [
|
|
199
|
+
{ role: 'system', content: systemContent },
|
|
200
|
+
{ role: 'assistant', content: report.report_content },
|
|
201
|
+
]).catch(err => console.error("Failed to save initial chat messages:", err));
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.error("Analysis failed:", err);
|
|
205
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
206
|
+
const isCancelled = errMsg.includes("Analysis cancelled") || errMsg.includes("aborted");
|
|
207
|
+
if (sessionIdRef.current === sid) {
|
|
208
|
+
setState((prev) => ({
|
|
209
|
+
...prev,
|
|
210
|
+
isAnalyzing: false,
|
|
211
|
+
streamingContent: "",
|
|
212
|
+
analysisError: isCancelled ? null : errMsg,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}, []);
|
|
217
|
+
|
|
218
|
+
// Cancel an in-progress analysis
|
|
219
|
+
const cancelAnalysis = useCallback(async (sid: string) => {
|
|
220
|
+
await window.electronAPI.cancelAnalysis(sid);
|
|
221
|
+
setState((prev) => ({
|
|
222
|
+
...prev,
|
|
223
|
+
isAnalyzing: false,
|
|
224
|
+
streamingContent: "",
|
|
225
|
+
analysisError: null,
|
|
226
|
+
}));
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
const chatHistoryRef = useRef<ChatMessage[]>([]);
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
chatHistoryRef.current = state.chatHistory;
|
|
232
|
+
}, [state.chatHistory]);
|
|
233
|
+
|
|
234
|
+
const sendFollowUp = useCallback(async (sid: string, message: string) => {
|
|
235
|
+
// Get the latest report ID for persisting chat messages
|
|
236
|
+
let currentReportId = '';
|
|
237
|
+
setState((prev) => {
|
|
238
|
+
if (prev.reports.length > 0) {
|
|
239
|
+
currentReportId = prev.reports[0].id;
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...prev,
|
|
243
|
+
isChatting: true,
|
|
244
|
+
chatError: null,
|
|
245
|
+
streamingContent: "",
|
|
246
|
+
chatHistory: [...prev.chatHistory, { role: 'user' as const, content: message }],
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const reply = await window.electronAPI.sendFollowUp(sid, currentReportId, chatHistoryRef.current, message);
|
|
252
|
+
|
|
253
|
+
if (sessionIdRef.current === sid) {
|
|
254
|
+
setState((prev) => ({
|
|
255
|
+
...prev,
|
|
256
|
+
isChatting: false,
|
|
257
|
+
streamingContent: "",
|
|
258
|
+
chatHistory: [...prev.chatHistory, { role: 'assistant' as const, content: reply }],
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error("Follow-up chat failed:", err);
|
|
263
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
264
|
+
if (sessionIdRef.current === sid) {
|
|
265
|
+
setState((prev) => ({
|
|
266
|
+
...prev,
|
|
267
|
+
isChatting: false,
|
|
268
|
+
streamingContent: "",
|
|
269
|
+
chatError: errMsg,
|
|
270
|
+
// Roll back the optimistically added user message on failure
|
|
271
|
+
chatHistory: prev.chatHistory.length > 0 && prev.chatHistory[prev.chatHistory.length - 1]?.role === 'user'
|
|
272
|
+
? prev.chatHistory.slice(0, -1)
|
|
273
|
+
: prev.chatHistory,
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
// Set up IPC event listeners for real-time updates
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
if (!sessionId) {
|
|
282
|
+
clearData();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Load initial data
|
|
287
|
+
loadData(sessionId);
|
|
288
|
+
|
|
289
|
+
// --- Batched request/hook/storage buffering for performance ---
|
|
290
|
+
const requestBuffer: CapturedRequest[] = [];
|
|
291
|
+
const hookBuffer: JsHookRecord[] = [];
|
|
292
|
+
const storageBuffer: StorageSnapshot[] = [];
|
|
293
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
294
|
+
|
|
295
|
+
const flush = () => {
|
|
296
|
+
if (requestBuffer.length > 0 || hookBuffer.length > 0 || storageBuffer.length > 0) {
|
|
297
|
+
const reqBatch = requestBuffer.splice(0);
|
|
298
|
+
const hookBatch = hookBuffer.splice(0);
|
|
299
|
+
const storageBatch = storageBuffer.splice(0);
|
|
300
|
+
setState((prev) => ({
|
|
301
|
+
...prev,
|
|
302
|
+
requests: reqBatch.length > 0 ? [...prev.requests, ...reqBatch] : prev.requests,
|
|
303
|
+
hooks: hookBatch.length > 0 ? [...hookBatch, ...prev.hooks] : prev.hooks,
|
|
304
|
+
snapshots: storageBatch.length > 0 ? [...prev.snapshots, ...storageBatch] : prev.snapshots,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
flushTimer = setInterval(flush, 300);
|
|
310
|
+
|
|
311
|
+
// Listen for new captured requests — buffer instead of immediate setState
|
|
312
|
+
const handleRequest = (data: CapturedRequest) => {
|
|
313
|
+
if (data.session_id !== sessionIdRef.current) return;
|
|
314
|
+
requestBuffer.push(data);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Listen for new hook records — buffer instead of immediate setState
|
|
318
|
+
const handleHook = (data: JsHookRecord) => {
|
|
319
|
+
if (data.session_id !== sessionIdRef.current) return;
|
|
320
|
+
hookBuffer.push(data);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Listen for new storage snapshots — buffer instead of immediate setState
|
|
324
|
+
const handleStorage = (data: StorageSnapshot) => {
|
|
325
|
+
if (data.session_id !== sessionIdRef.current) return;
|
|
326
|
+
storageBuffer.push(data);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Listen for analysis progress (streaming chunks)
|
|
330
|
+
const handleAnalysisProgress = (chunk: string) => {
|
|
331
|
+
setState((prev) => ({
|
|
332
|
+
...prev,
|
|
333
|
+
streamingContent: prev.streamingContent + chunk,
|
|
334
|
+
}));
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
window.electronAPI.onRequestCaptured(handleRequest);
|
|
338
|
+
window.electronAPI.onHookCaptured(handleHook);
|
|
339
|
+
window.electronAPI.onStorageCaptured(handleStorage);
|
|
340
|
+
window.electronAPI.onAnalysisProgress(handleAnalysisProgress);
|
|
341
|
+
|
|
342
|
+
// Listen for interaction recording events (debounced to avoid excessive DB queries)
|
|
343
|
+
let interactionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
344
|
+
window.electronAPI.onInteractionRecorded(() => {
|
|
345
|
+
if (interactionDebounceTimer) clearTimeout(interactionDebounceTimer);
|
|
346
|
+
interactionDebounceTimer = setTimeout(() => {
|
|
347
|
+
if (sessionIdRef.current) {
|
|
348
|
+
window.electronAPI.getInteractions(sessionIdRef.current).then((interactions: InteractionEvent[]) => {
|
|
349
|
+
setState((prev) => ({
|
|
350
|
+
...prev,
|
|
351
|
+
interactions: (interactions || []).sort((a, b) => a.sequence - b.sequence),
|
|
352
|
+
}));
|
|
353
|
+
}).catch(() => {});
|
|
354
|
+
}
|
|
355
|
+
}, 500);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Cleanup listeners on unmount or session change
|
|
359
|
+
return () => {
|
|
360
|
+
if (flushTimer) clearInterval(flushTimer);
|
|
361
|
+
if (interactionDebounceTimer) clearTimeout(interactionDebounceTimer);
|
|
362
|
+
flush(); // flush remaining buffered items
|
|
363
|
+
window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_REQUEST);
|
|
364
|
+
window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_HOOK);
|
|
365
|
+
window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_STORAGE);
|
|
366
|
+
window.electronAPI.removeAllListeners(IPC_CHANNELS.AI_PROGRESS);
|
|
367
|
+
window.electronAPI.removeAllListeners('interaction:recorded');
|
|
368
|
+
};
|
|
369
|
+
}, [sessionId, loadData, clearData]);
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
...state,
|
|
373
|
+
loadData,
|
|
374
|
+
clearData,
|
|
375
|
+
clearCaptureData,
|
|
376
|
+
selectRequest,
|
|
377
|
+
startAnalysis,
|
|
378
|
+
cancelAnalysis,
|
|
379
|
+
sendFollowUp,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export default useCapture;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState, useCallback, useRef } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { Button } from '../ui'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* useConfirm — lightweight confirmation dialog rendered via Portal.
|
|
7
|
+
* Cross-platform safe (no window.confirm dependency).
|
|
8
|
+
*/
|
|
9
|
+
export function useConfirm() {
|
|
10
|
+
const [state, setState] = useState<{
|
|
11
|
+
message: string
|
|
12
|
+
okText?: string
|
|
13
|
+
cancelText?: string
|
|
14
|
+
} | null>(null)
|
|
15
|
+
|
|
16
|
+
const resolveRef = useRef<((ok: boolean) => void) | null>(null)
|
|
17
|
+
|
|
18
|
+
const confirm = useCallback(
|
|
19
|
+
(message: string, opts?: { okText?: string; cancelText?: string }): Promise<boolean> => {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
resolveRef.current = resolve
|
|
22
|
+
setState({ message, ...opts })
|
|
23
|
+
})
|
|
24
|
+
},
|
|
25
|
+
[],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const handleOk = useCallback(() => {
|
|
29
|
+
resolveRef.current?.(true)
|
|
30
|
+
resolveRef.current = null
|
|
31
|
+
setState(null)
|
|
32
|
+
}, [])
|
|
33
|
+
|
|
34
|
+
const handleCancel = useCallback(() => {
|
|
35
|
+
resolveRef.current?.(false)
|
|
36
|
+
resolveRef.current = null
|
|
37
|
+
setState(null)
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const ConfirmDialog = state
|
|
41
|
+
? createPortal(
|
|
42
|
+
<div
|
|
43
|
+
style={{
|
|
44
|
+
position: 'fixed',
|
|
45
|
+
inset: 0,
|
|
46
|
+
zIndex: 9999,
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
background: 'rgba(0,0,0,0.35)',
|
|
51
|
+
}}
|
|
52
|
+
onClick={handleCancel}
|
|
53
|
+
>
|
|
54
|
+
<div
|
|
55
|
+
style={{
|
|
56
|
+
background: 'var(--color-frame)',
|
|
57
|
+
border: '1px solid var(--color-border)',
|
|
58
|
+
borderRadius: 'var(--radius-lg)',
|
|
59
|
+
boxShadow: 'var(--shadow-lg)',
|
|
60
|
+
padding: '20px 24px',
|
|
61
|
+
minWidth: 280,
|
|
62
|
+
maxWidth: 400,
|
|
63
|
+
}}
|
|
64
|
+
onClick={(e) => e.stopPropagation()}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
fontSize: 'var(--font-size-base)',
|
|
69
|
+
color: 'var(--text-secondary)',
|
|
70
|
+
lineHeight: 'var(--line-height-normal)',
|
|
71
|
+
marginBottom: 16,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{state.message}
|
|
75
|
+
</div>
|
|
76
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
|
77
|
+
<Button size="sm" onClick={handleCancel}>
|
|
78
|
+
{state.cancelText || 'Cancel'}
|
|
79
|
+
</Button>
|
|
80
|
+
<Button size="sm" variant="danger" onClick={handleOk}>
|
|
81
|
+
{state.okText || 'OK'}
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>,
|
|
86
|
+
document.body,
|
|
87
|
+
)
|
|
88
|
+
: null
|
|
89
|
+
|
|
90
|
+
return { confirm, ConfirmDialog }
|
|
91
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useMemo } from 'react'
|
|
2
|
+
import type { Session } from '../../shared/types'
|
|
3
|
+
|
|
4
|
+
export interface UseSessionReturn {
|
|
5
|
+
sessions: Session[]
|
|
6
|
+
currentSessionId: string | null
|
|
7
|
+
currentSession: Session | null
|
|
8
|
+
loading: boolean
|
|
9
|
+
loadSessions: () => Promise<void>
|
|
10
|
+
createSession: (name: string, url: string) => Promise<void>
|
|
11
|
+
selectSession: (id: string | null) => void
|
|
12
|
+
deleteSession: (id: string) => Promise<void>
|
|
13
|
+
startCapture: () => Promise<void>
|
|
14
|
+
resumeCapture: () => Promise<void>
|
|
15
|
+
pauseCapture: () => Promise<void>
|
|
16
|
+
stopCapture: () => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useSession(): UseSessionReturn {
|
|
20
|
+
const [sessions, setSessions] = useState<Session[]>([])
|
|
21
|
+
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null)
|
|
22
|
+
const [loading, setLoading] = useState(false)
|
|
23
|
+
|
|
24
|
+
const currentSession = useMemo(
|
|
25
|
+
() => sessions.find((s) => s.id === currentSessionId) ?? null,
|
|
26
|
+
[sessions, currentSessionId]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const loadSessions = useCallback(async () => {
|
|
30
|
+
try {
|
|
31
|
+
setLoading(true)
|
|
32
|
+
const list = await window.electronAPI.listSessions()
|
|
33
|
+
setSessions(list)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Failed to load sessions:', err)
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false)
|
|
38
|
+
}
|
|
39
|
+
}, [])
|
|
40
|
+
|
|
41
|
+
const createSession = useCallback(async (name: string, url: string) => {
|
|
42
|
+
try {
|
|
43
|
+
const session = await window.electronAPI.createSession(name, url)
|
|
44
|
+
setSessions((prev) => [...prev, session])
|
|
45
|
+
setCurrentSessionId(session.id)
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Failed to create session:', err)
|
|
48
|
+
throw err
|
|
49
|
+
}
|
|
50
|
+
}, [])
|
|
51
|
+
|
|
52
|
+
const selectSession = useCallback((id: string | null) => {
|
|
53
|
+
setCurrentSessionId(id)
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
const deleteSession = useCallback(
|
|
57
|
+
async (id: string) => {
|
|
58
|
+
try {
|
|
59
|
+
await window.electronAPI.deleteSession(id)
|
|
60
|
+
setSessions((prev) => prev.filter((s) => s.id !== id))
|
|
61
|
+
if (currentSessionId === id) {
|
|
62
|
+
setCurrentSessionId(null)
|
|
63
|
+
}
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('Failed to delete session:', err)
|
|
66
|
+
throw err
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
[currentSessionId]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const startCapture = useCallback(async () => {
|
|
73
|
+
if (!currentSessionId) return
|
|
74
|
+
try {
|
|
75
|
+
await window.electronAPI.startCapture(currentSessionId)
|
|
76
|
+
// Reload from DB to ensure UI stays in sync (avoids stale closure issues)
|
|
77
|
+
await loadSessions()
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('Failed to start capture:', err)
|
|
80
|
+
throw err
|
|
81
|
+
}
|
|
82
|
+
}, [currentSessionId, loadSessions])
|
|
83
|
+
|
|
84
|
+
const resumeCapture = useCallback(async () => {
|
|
85
|
+
if (!currentSessionId) return
|
|
86
|
+
try {
|
|
87
|
+
await window.electronAPI.resumeCapture(currentSessionId)
|
|
88
|
+
await loadSessions()
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error('Failed to resume capture:', err)
|
|
91
|
+
throw err
|
|
92
|
+
}
|
|
93
|
+
}, [currentSessionId, loadSessions])
|
|
94
|
+
|
|
95
|
+
const pauseCapture = useCallback(async () => {
|
|
96
|
+
if (!currentSessionId) return
|
|
97
|
+
try {
|
|
98
|
+
await window.electronAPI.pauseCapture(currentSessionId)
|
|
99
|
+
await loadSessions()
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to pause capture:', err)
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
}, [currentSessionId, loadSessions])
|
|
105
|
+
|
|
106
|
+
const stopCapture = useCallback(async () => {
|
|
107
|
+
if (!currentSessionId) return
|
|
108
|
+
try {
|
|
109
|
+
await window.electronAPI.stopCapture(currentSessionId)
|
|
110
|
+
await loadSessions()
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('Failed to stop capture:', err)
|
|
113
|
+
throw err
|
|
114
|
+
}
|
|
115
|
+
}, [currentSessionId, loadSessions])
|
|
116
|
+
|
|
117
|
+
// Load sessions on mount
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
loadSessions()
|
|
120
|
+
}, [loadSessions])
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
sessions,
|
|
124
|
+
currentSessionId,
|
|
125
|
+
currentSession,
|
|
126
|
+
loading,
|
|
127
|
+
loadSessions,
|
|
128
|
+
createSession,
|
|
129
|
+
selectSession,
|
|
130
|
+
deleteSession,
|
|
131
|
+
startCapture,
|
|
132
|
+
resumeCapture,
|
|
133
|
+
pauseCapture,
|
|
134
|
+
stopCapture
|
|
135
|
+
}
|
|
136
|
+
}
|