chatbot-analyze-qweasd 1.0.0
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/apiflow5-gui.mjs +1383 -0
- package/apiflow5.mjs +1036 -0
- package/package.json +12 -0
package/apiflow5-gui.mjs
ADDED
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* apiflow5 GUI Server (with SSE progress)
|
|
4
|
+
* 啟動方式: node apiflow5-gui.mjs
|
|
5
|
+
* 瀏覽器開啟: http://localhost:3939
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PORT = 3939;
|
|
16
|
+
const WORKSPACE = process.env.WORKSPACE || "/Users/Timothy/IdeaProjects";
|
|
17
|
+
|
|
18
|
+
// 自動掃描 workspace 中的 project 資料夾
|
|
19
|
+
function scanProjects(workspace) {
|
|
20
|
+
const projects = [];
|
|
21
|
+
const ignoreList = [".git", ".idea", "node_modules", "dist", ".claude", ".vscode", ".gradle", "build"];
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const entries = readdirSync(workspace);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (ignoreList.includes(entry) || entry.startsWith(".")) continue;
|
|
27
|
+
const fullPath = path.join(workspace, entry);
|
|
28
|
+
try {
|
|
29
|
+
if (statSync(fullPath).isDirectory()) {
|
|
30
|
+
const hasSrc = existsSync(path.join(fullPath, "src"));
|
|
31
|
+
const hasPom = existsSync(path.join(fullPath, "pom.xml"));
|
|
32
|
+
const hasGradle = existsSync(path.join(fullPath, "build.gradle"));
|
|
33
|
+
if (hasSrc || hasPom || hasGradle) {
|
|
34
|
+
projects.push(entry);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
41
|
+
return projects.sort();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 歷史記錄管理
|
|
45
|
+
const HISTORY_FILE = path.join(WORKSPACE, "dist", "apiflow5-history.json");
|
|
46
|
+
|
|
47
|
+
function loadHistory() {
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(HISTORY_FILE)) {
|
|
50
|
+
return JSON.parse(readFileSync(HISTORY_FILE, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function saveHistory(history) {
|
|
57
|
+
try {
|
|
58
|
+
mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
59
|
+
writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2), "utf8");
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("[History] Save error:", err.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addToHistory({ project, functionPath, svgPath, ir, stage, nodeId, nodeLabel, nodeFocus }) {
|
|
66
|
+
const history = loadHistory();
|
|
67
|
+
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
68
|
+
const entry = {
|
|
69
|
+
id,
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
project,
|
|
72
|
+
functionPath,
|
|
73
|
+
svgPath,
|
|
74
|
+
ir,
|
|
75
|
+
stage: stage || 1,
|
|
76
|
+
nodeId: nodeId || null,
|
|
77
|
+
nodeLabel: nodeLabel || null,
|
|
78
|
+
nodeFocus: nodeFocus || null
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 檢查是否已存在相同的 project + functionPath
|
|
82
|
+
const existingIdx = history.findIndex(h =>
|
|
83
|
+
h.project === project &&
|
|
84
|
+
h.functionPath === functionPath &&
|
|
85
|
+
(h.stage || 1) === (stage || 1) &&
|
|
86
|
+
(h.nodeId || null) === (nodeId || null)
|
|
87
|
+
);
|
|
88
|
+
if (existingIdx >= 0) {
|
|
89
|
+
// 更新現有記錄
|
|
90
|
+
history[existingIdx] = entry;
|
|
91
|
+
} else {
|
|
92
|
+
// 新增到最前面
|
|
93
|
+
history.unshift(entry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 只保留最近 50 筆
|
|
97
|
+
const trimmed = history.slice(0, 50);
|
|
98
|
+
saveHistory(trimmed);
|
|
99
|
+
return entry;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// SSE helper
|
|
103
|
+
function sendSSE(res, event, data) {
|
|
104
|
+
res.write(`event: ${event}\n`);
|
|
105
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 檢查 IDEA 是否正在運行
|
|
109
|
+
function checkIdeaRunning() {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const p = spawn("pgrep", ["-f", "IntelliJ IDEA"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
112
|
+
let out = "";
|
|
113
|
+
p.stdout.on("data", (d) => (out += d.toString()));
|
|
114
|
+
p.on("close", (code) => {
|
|
115
|
+
resolve(code === 0 && out.trim().length > 0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 在 IDEA 中開啟檔案
|
|
121
|
+
async function openInIdea(filePath, line) {
|
|
122
|
+
const wasRunning = await checkIdeaRunning();
|
|
123
|
+
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
// 使用 IDEA URL scheme(最可靠的方法)
|
|
126
|
+
// idea://open?file=/path/to/file&line=42
|
|
127
|
+
let ideaUrl = "idea://open?file=" + encodeURIComponent(filePath);
|
|
128
|
+
if (line) {
|
|
129
|
+
ideaUrl += "&line=" + line;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log("[OpenIDEA] URL: " + ideaUrl);
|
|
133
|
+
|
|
134
|
+
const p = spawn("open", [ideaUrl], { stdio: "ignore" });
|
|
135
|
+
|
|
136
|
+
p.on("error", (err) => {
|
|
137
|
+
reject(err);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
p.on("close", (code) => {
|
|
141
|
+
if (code === 0) {
|
|
142
|
+
resolve({ wasRunning, method: "url-scheme" });
|
|
143
|
+
} else {
|
|
144
|
+
reject(new Error("open command failed with code " + code));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 執行 apiflow 並透過 SSE 回報進度
|
|
151
|
+
function runApiflowWithProgress(res, { project, functionPath, workspace, stage, nodeId, nodeLabel, nodeFocus }) {
|
|
152
|
+
const apiflowPath = path.join(__dirname, "apiflow5.mjs");
|
|
153
|
+
const outPath = path.join(workspace, "dist", `flow-stage${stage || 1}-${Date.now()}.svg`);
|
|
154
|
+
|
|
155
|
+
const args = [
|
|
156
|
+
apiflowPath,
|
|
157
|
+
"--folder", workspace,
|
|
158
|
+
"--project", project,
|
|
159
|
+
"--path", functionPath,
|
|
160
|
+
"--out", outPath,
|
|
161
|
+
"--keep",
|
|
162
|
+
"--stage", String(stage || 1)
|
|
163
|
+
];
|
|
164
|
+
if (stage === 2 && nodeId) {
|
|
165
|
+
args.push("--node", nodeId);
|
|
166
|
+
if (nodeLabel) args.push("--node-label", nodeLabel);
|
|
167
|
+
if (nodeFocus) args.push("--node-focus", nodeFocus);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
sendSSE(res, "progress", { stage: 0, percent: 0, message: "啟動中..." });
|
|
171
|
+
|
|
172
|
+
const p = spawn("node", args, { cwd: workspace });
|
|
173
|
+
let stdout = "";
|
|
174
|
+
let lastPercent = 0;
|
|
175
|
+
|
|
176
|
+
// 捕獲 stderr,解析 [PROGRESS] 訊息
|
|
177
|
+
p.stderr.on("data", (chunk) => {
|
|
178
|
+
const lines = chunk.toString().split("\n");
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (line.startsWith("[PROGRESS]")) {
|
|
181
|
+
try {
|
|
182
|
+
const json = line.replace("[PROGRESS]", "").trim();
|
|
183
|
+
const progress = JSON.parse(json);
|
|
184
|
+
lastPercent = progress.percent;
|
|
185
|
+
sendSSE(res, "progress", progress);
|
|
186
|
+
} catch {}
|
|
187
|
+
} else if (line.trim()) {
|
|
188
|
+
// 其他 stderr 輸出當作 log
|
|
189
|
+
sendSSE(res, "log", { message: line.trim() });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
p.stdout.on("data", (d) => {
|
|
195
|
+
stdout += d.toString();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
p.on("close", (code) => {
|
|
199
|
+
if (code !== 0) {
|
|
200
|
+
sendSSE(res, "error", { message: `執行失敗 (exit code: ${code})` });
|
|
201
|
+
} else {
|
|
202
|
+
// 讀取 IR 資料以提供 function 列表
|
|
203
|
+
let ir = null;
|
|
204
|
+
const irPath = path.join(workspace, "dist", "ir.json");
|
|
205
|
+
try {
|
|
206
|
+
if (existsSync(irPath)) {
|
|
207
|
+
ir = JSON.parse(readFileSync(irPath, "utf8"));
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
|
|
211
|
+
// 保存到歷史記錄
|
|
212
|
+
const historyEntry = addToHistory({
|
|
213
|
+
project,
|
|
214
|
+
functionPath,
|
|
215
|
+
svgPath: outPath,
|
|
216
|
+
ir,
|
|
217
|
+
stage,
|
|
218
|
+
nodeId,
|
|
219
|
+
nodeLabel,
|
|
220
|
+
nodeFocus
|
|
221
|
+
});
|
|
222
|
+
console.log("[History] Saved: " + project + " / " + functionPath);
|
|
223
|
+
|
|
224
|
+
sendSSE(res, "done", { svgPath: outPath, ir, historyId: historyEntry.id });
|
|
225
|
+
}
|
|
226
|
+
res.end();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
p.on("error", (err) => {
|
|
230
|
+
sendSSE(res, "error", { message: err.message });
|
|
231
|
+
res.end();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Timeout: 15 分鐘
|
|
235
|
+
const timeout = setTimeout(() => {
|
|
236
|
+
p.kill();
|
|
237
|
+
sendSSE(res, "error", { message: "Timeout: 執行超過 15 分鐘" });
|
|
238
|
+
res.end();
|
|
239
|
+
}, 15 * 60 * 1000);
|
|
240
|
+
|
|
241
|
+
p.on("close", () => clearTimeout(timeout));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const HTML_PAGE = `<!DOCTYPE html>
|
|
245
|
+
<html lang="zh-TW">
|
|
246
|
+
<head>
|
|
247
|
+
<meta charset="UTF-8">
|
|
248
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
249
|
+
<title>API Flow (Staged) Generator</title>
|
|
250
|
+
<style>
|
|
251
|
+
* { box-sizing: border-box; }
|
|
252
|
+
body {
|
|
253
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
254
|
+
max-width: 1200px;
|
|
255
|
+
margin: 0 auto;
|
|
256
|
+
padding: 20px;
|
|
257
|
+
background: #f5f5f5;
|
|
258
|
+
}
|
|
259
|
+
h1 { color: #333; margin-bottom: 30px; }
|
|
260
|
+
.form-group { margin-bottom: 20px; }
|
|
261
|
+
label {
|
|
262
|
+
display: block;
|
|
263
|
+
margin-bottom: 8px;
|
|
264
|
+
font-weight: 600;
|
|
265
|
+
color: #555;
|
|
266
|
+
}
|
|
267
|
+
select, input[type="text"] {
|
|
268
|
+
width: 100%;
|
|
269
|
+
padding: 12px;
|
|
270
|
+
font-size: 16px;
|
|
271
|
+
border: 1px solid #ddd;
|
|
272
|
+
border-radius: 6px;
|
|
273
|
+
background: white;
|
|
274
|
+
}
|
|
275
|
+
select:focus, input:focus {
|
|
276
|
+
outline: none;
|
|
277
|
+
border-color: #007aff;
|
|
278
|
+
box-shadow: 0 0 0 3px rgba(0,122,255,0.1);
|
|
279
|
+
}
|
|
280
|
+
.hint {
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
color: #888;
|
|
283
|
+
margin-top: 6px;
|
|
284
|
+
}
|
|
285
|
+
button {
|
|
286
|
+
background: #007aff;
|
|
287
|
+
color: white;
|
|
288
|
+
border: none;
|
|
289
|
+
padding: 14px 32px;
|
|
290
|
+
font-size: 16px;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
border-radius: 6px;
|
|
293
|
+
cursor: pointer;
|
|
294
|
+
transition: background 0.2s;
|
|
295
|
+
}
|
|
296
|
+
button:hover { background: #0056b3; }
|
|
297
|
+
button:disabled {
|
|
298
|
+
background: #ccc;
|
|
299
|
+
cursor: not-allowed;
|
|
300
|
+
}
|
|
301
|
+
.result {
|
|
302
|
+
margin-top: 30px;
|
|
303
|
+
padding: 20px;
|
|
304
|
+
background: white;
|
|
305
|
+
border-radius: 8px;
|
|
306
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
307
|
+
}
|
|
308
|
+
.result h3 { margin-top: 0; }
|
|
309
|
+
|
|
310
|
+
/* Progress Section */
|
|
311
|
+
.progress-section {
|
|
312
|
+
margin-bottom: 20px;
|
|
313
|
+
}
|
|
314
|
+
.progress-bar-container {
|
|
315
|
+
background: #e9ecef;
|
|
316
|
+
border-radius: 10px;
|
|
317
|
+
height: 24px;
|
|
318
|
+
overflow: hidden;
|
|
319
|
+
margin-bottom: 10px;
|
|
320
|
+
}
|
|
321
|
+
.progress-bar {
|
|
322
|
+
height: 100%;
|
|
323
|
+
background: linear-gradient(90deg, #007aff, #00c6ff);
|
|
324
|
+
border-radius: 10px;
|
|
325
|
+
transition: width 0.3s ease;
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
color: white;
|
|
330
|
+
font-size: 12px;
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
min-width: 40px;
|
|
333
|
+
}
|
|
334
|
+
.progress-message {
|
|
335
|
+
font-size: 14px;
|
|
336
|
+
color: #666;
|
|
337
|
+
margin-bottom: 10px;
|
|
338
|
+
}
|
|
339
|
+
.progress-stages {
|
|
340
|
+
display: flex;
|
|
341
|
+
justify-content: space-between;
|
|
342
|
+
font-size: 11px;
|
|
343
|
+
color: #999;
|
|
344
|
+
margin-top: 5px;
|
|
345
|
+
}
|
|
346
|
+
.progress-stages span {
|
|
347
|
+
flex: 1;
|
|
348
|
+
text-align: center;
|
|
349
|
+
padding: 4px;
|
|
350
|
+
border-radius: 4px;
|
|
351
|
+
transition: all 0.3s;
|
|
352
|
+
}
|
|
353
|
+
.progress-stages span.active {
|
|
354
|
+
background: #e7f3ff;
|
|
355
|
+
color: #007aff;
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
}
|
|
358
|
+
.progress-stages span.done {
|
|
359
|
+
background: #d4edda;
|
|
360
|
+
color: #155724;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Log Section */
|
|
364
|
+
.log-section {
|
|
365
|
+
background: #1e1e1e;
|
|
366
|
+
border-radius: 6px;
|
|
367
|
+
padding: 12px;
|
|
368
|
+
max-height: 150px;
|
|
369
|
+
overflow-y: auto;
|
|
370
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
371
|
+
font-size: 12px;
|
|
372
|
+
margin-bottom: 15px;
|
|
373
|
+
}
|
|
374
|
+
.log-section:empty { display: none; }
|
|
375
|
+
.log-line {
|
|
376
|
+
color: #9cdcfe;
|
|
377
|
+
margin: 2px 0;
|
|
378
|
+
white-space: pre-wrap;
|
|
379
|
+
word-break: break-all;
|
|
380
|
+
}
|
|
381
|
+
.log-line.error { color: #f48771; }
|
|
382
|
+
|
|
383
|
+
.status {
|
|
384
|
+
padding: 12px 16px;
|
|
385
|
+
border-radius: 6px;
|
|
386
|
+
margin-bottom: 15px;
|
|
387
|
+
}
|
|
388
|
+
.status.success { background: #d4edda; color: #155724; }
|
|
389
|
+
.status.error { background: #f8d7da; color: #721c24; }
|
|
390
|
+
.svg-container {
|
|
391
|
+
margin-top: 20px;
|
|
392
|
+
overflow-x: auto;
|
|
393
|
+
background: white;
|
|
394
|
+
padding: 20px;
|
|
395
|
+
border: 1px solid #eee;
|
|
396
|
+
border-radius: 6px;
|
|
397
|
+
}
|
|
398
|
+
.svg-container img {
|
|
399
|
+
max-width: 100%;
|
|
400
|
+
height: auto;
|
|
401
|
+
}
|
|
402
|
+
.zoom-controls {
|
|
403
|
+
display: flex;
|
|
404
|
+
align-items: center;
|
|
405
|
+
gap: 10px;
|
|
406
|
+
margin-top: 10px;
|
|
407
|
+
}
|
|
408
|
+
.zoom-controls button {
|
|
409
|
+
padding: 6px 10px;
|
|
410
|
+
font-size: 12px;
|
|
411
|
+
border-radius: 4px;
|
|
412
|
+
}
|
|
413
|
+
.zoom-controls input[type="range"] {
|
|
414
|
+
width: 200px;
|
|
415
|
+
}
|
|
416
|
+
.zoom-controls .zoom-label {
|
|
417
|
+
font-size: 12px;
|
|
418
|
+
color: #555;
|
|
419
|
+
min-width: 60px;
|
|
420
|
+
text-align: right;
|
|
421
|
+
}
|
|
422
|
+
.command-preview {
|
|
423
|
+
background: #2d2d2d;
|
|
424
|
+
color: #f8f8f2;
|
|
425
|
+
padding: 15px;
|
|
426
|
+
border-radius: 6px;
|
|
427
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
428
|
+
font-size: 13px;
|
|
429
|
+
overflow-x: auto;
|
|
430
|
+
white-space: pre-wrap;
|
|
431
|
+
word-break: break-all;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Function List */
|
|
435
|
+
.function-list {
|
|
436
|
+
margin-top: 20px;
|
|
437
|
+
border: 1px solid #e0e0e0;
|
|
438
|
+
border-radius: 8px;
|
|
439
|
+
overflow: hidden;
|
|
440
|
+
}
|
|
441
|
+
.function-list h4 {
|
|
442
|
+
margin: 0;
|
|
443
|
+
padding: 12px 16px;
|
|
444
|
+
background: #f8f9fa;
|
|
445
|
+
border-bottom: 1px solid #e0e0e0;
|
|
446
|
+
font-size: 14px;
|
|
447
|
+
}
|
|
448
|
+
.function-list-empty {
|
|
449
|
+
padding: 20px;
|
|
450
|
+
text-align: center;
|
|
451
|
+
color: #888;
|
|
452
|
+
}
|
|
453
|
+
.function-item {
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
padding: 10px 16px;
|
|
457
|
+
border-bottom: 1px solid #f0f0f0;
|
|
458
|
+
transition: background 0.15s;
|
|
459
|
+
}
|
|
460
|
+
.function-item:last-child { border-bottom: none; }
|
|
461
|
+
.function-item:hover { background: #f8f9fa; }
|
|
462
|
+
.function-item .info { flex: 1; }
|
|
463
|
+
.function-item .name {
|
|
464
|
+
font-weight: 600;
|
|
465
|
+
color: #333;
|
|
466
|
+
font-size: 14px;
|
|
467
|
+
}
|
|
468
|
+
.function-item .location {
|
|
469
|
+
font-size: 12px;
|
|
470
|
+
color: #888;
|
|
471
|
+
font-family: "SF Mono", Monaco, monospace;
|
|
472
|
+
margin-top: 2px;
|
|
473
|
+
}
|
|
474
|
+
.function-item .type-badge {
|
|
475
|
+
font-size: 11px;
|
|
476
|
+
padding: 2px 8px;
|
|
477
|
+
border-radius: 10px;
|
|
478
|
+
margin-right: 10px;
|
|
479
|
+
font-weight: 500;
|
|
480
|
+
}
|
|
481
|
+
.type-badge.http { background: #e3f2fd; color: #1565c0; }
|
|
482
|
+
.type-badge.db { background: #fff3e0; color: #e65100; }
|
|
483
|
+
.type-badge.external { background: #fce4ec; color: #c2185b; }
|
|
484
|
+
.type-badge.method { background: #e8f5e9; color: #2e7d32; }
|
|
485
|
+
.function-item .open-btn {
|
|
486
|
+
background: #007aff;
|
|
487
|
+
color: white;
|
|
488
|
+
border: none;
|
|
489
|
+
padding: 6px 12px;
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
border-radius: 4px;
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
transition: background 0.2s;
|
|
494
|
+
}
|
|
495
|
+
.function-item .open-btn:hover { background: #0056b3; }
|
|
496
|
+
.function-item .open-btn:disabled {
|
|
497
|
+
background: #ccc;
|
|
498
|
+
cursor: not-allowed;
|
|
499
|
+
}
|
|
500
|
+
.function-item .open-btn.success {
|
|
501
|
+
background: #28a745;
|
|
502
|
+
}
|
|
503
|
+
.function-item .explore-btn {
|
|
504
|
+
background: #2f855a;
|
|
505
|
+
margin-right: 8px;
|
|
506
|
+
}
|
|
507
|
+
.function-item .explore-btn:hover { background: #276749; }
|
|
508
|
+
|
|
509
|
+
/* Toast */
|
|
510
|
+
.toast {
|
|
511
|
+
position: fixed;
|
|
512
|
+
bottom: 20px;
|
|
513
|
+
right: 20px;
|
|
514
|
+
background: #333;
|
|
515
|
+
color: white;
|
|
516
|
+
padding: 12px 20px;
|
|
517
|
+
border-radius: 8px;
|
|
518
|
+
font-size: 14px;
|
|
519
|
+
opacity: 0;
|
|
520
|
+
transform: translateY(20px);
|
|
521
|
+
transition: all 0.3s ease;
|
|
522
|
+
z-index: 1000;
|
|
523
|
+
}
|
|
524
|
+
.toast.show {
|
|
525
|
+
opacity: 1;
|
|
526
|
+
transform: translateY(0);
|
|
527
|
+
}
|
|
528
|
+
</style>
|
|
529
|
+
</head>
|
|
530
|
+
<body>
|
|
531
|
+
<h1>API Flow (Staged) Generator</h1>
|
|
532
|
+
|
|
533
|
+
<div class="form-group">
|
|
534
|
+
<label for="project">Project</label>
|
|
535
|
+
<select id="project">
|
|
536
|
+
<option value="">-- 選擇專案 --</option>
|
|
537
|
+
</select>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<div class="form-group">
|
|
541
|
+
<label for="functionPath">Function Path</label>
|
|
542
|
+
<input type="text" id="functionPath"
|
|
543
|
+
placeholder="例: ai.omnichat.service.AiService#enterAiSession">
|
|
544
|
+
<div class="hint">格式: package.ClassName#methodName</div>
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<div class="form-group">
|
|
548
|
+
<label>Stage</label>
|
|
549
|
+
<div style="display:flex;gap:12px;align-items:center;">
|
|
550
|
+
<label style="display:flex;align-items:center;gap:6px;font-weight:500;">
|
|
551
|
+
<input type="radio" name="stageSelect" value="1" checked onchange="setStage(1, null)">
|
|
552
|
+
Stage 1
|
|
553
|
+
</label>
|
|
554
|
+
<label style="display:flex;align-items:center;gap:6px;font-weight:500;">
|
|
555
|
+
<input type="radio" name="stageSelect" value="2" onchange="setStage(2, currentNode)">
|
|
556
|
+
Stage 2
|
|
557
|
+
</label>
|
|
558
|
+
<div class="hint">Stage 2 需先選擇細化節點</div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="form-group">
|
|
563
|
+
<label for="history">或從歷史記錄載入</label>
|
|
564
|
+
<select id="history" onchange="loadFromHistory()">
|
|
565
|
+
<option value="">-- 選擇歷史記錄 --</option>
|
|
566
|
+
</select>
|
|
567
|
+
<div class="hint">選擇後直接顯示結果,不需重新分析</div>
|
|
568
|
+
</div>
|
|
569
|
+
|
|
570
|
+
<div class="form-group">
|
|
571
|
+
<label>指令預覽</label>
|
|
572
|
+
<div class="command-preview" id="commandPreview">請選擇專案並輸入 function path</div>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<button id="generateBtn" onclick="generate()">產生 Stage 1</button>
|
|
576
|
+
<button id="regenerateBtn" onclick="generate(true)">重新產生</button>
|
|
577
|
+
|
|
578
|
+
<div class="result" id="result" style="display:none;">
|
|
579
|
+
<h3>執行進度</h3>
|
|
580
|
+
|
|
581
|
+
<div class="progress-section">
|
|
582
|
+
<div class="progress-bar-container">
|
|
583
|
+
<div class="progress-bar" id="progressBar" style="width: 0%">0%</div>
|
|
584
|
+
</div>
|
|
585
|
+
<div class="progress-message" id="progressMessage">準備中...</div>
|
|
586
|
+
<div class="progress-stages" id="progressStages">
|
|
587
|
+
<span data-stage="1">1. 提取程式碼</span>
|
|
588
|
+
<span data-stage="2">2. Codex 分析</span>
|
|
589
|
+
<span data-stage="3">3. 生成圖表</span>
|
|
590
|
+
<span data-stage="4">4. 轉換 SVG</span>
|
|
591
|
+
<span data-stage="5">5. 完成</span>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<div class="log-section" id="logSection"></div>
|
|
596
|
+
|
|
597
|
+
<div class="status" id="status" style="display:none;"></div>
|
|
598
|
+
<div class="svg-container" id="svgContainer"></div>
|
|
599
|
+
<div class="zoom-controls" id="zoomControls" style="display:none;">
|
|
600
|
+
<button type="button" onclick="zoomOut()">-</button>
|
|
601
|
+
<input type="range" id="zoomRange" min="0.5" max="10" step="0.1" value="1" oninput="zoomTo(this.value)">
|
|
602
|
+
<button type="button" onclick="zoomIn()">+</button>
|
|
603
|
+
<button type="button" onclick="zoomReset()">Reset</button>
|
|
604
|
+
<div class="zoom-label" id="zoomLabel">100%</div>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
<div class="function-list" id="functionList" style="display:none;">
|
|
608
|
+
<h4>呼叫清單 (點擊開啟 IDEA)</h4>
|
|
609
|
+
<div id="functionListContent"></div>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<div class="function-list" id="refineList" style="display:none;">
|
|
613
|
+
<h4>細化節點 (Stage 2)</h4>
|
|
614
|
+
<div id="refineListContent"></div>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<div class="toast" id="toast"></div>
|
|
619
|
+
|
|
620
|
+
<script>
|
|
621
|
+
const WORKSPACE = '${WORKSPACE}';
|
|
622
|
+
var currentStage = 1;
|
|
623
|
+
var currentNode = null;
|
|
624
|
+
|
|
625
|
+
// 載入 projects
|
|
626
|
+
var projectsPromise = fetch('/api/projects')
|
|
627
|
+
.then(r => r.json())
|
|
628
|
+
.then(projects => {
|
|
629
|
+
const select = document.getElementById('project');
|
|
630
|
+
projects.forEach(p => {
|
|
631
|
+
const opt = document.createElement('option');
|
|
632
|
+
opt.value = p;
|
|
633
|
+
opt.textContent = p;
|
|
634
|
+
select.appendChild(opt);
|
|
635
|
+
});
|
|
636
|
+
return projects;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// 載入歷史記錄
|
|
640
|
+
function refreshHistory() {
|
|
641
|
+
return fetch('/api/history')
|
|
642
|
+
.then(r => r.json())
|
|
643
|
+
.then(history => {
|
|
644
|
+
const select = document.getElementById('history');
|
|
645
|
+
// 清空現有選項(保留第一個)
|
|
646
|
+
while (select.options.length > 1) {
|
|
647
|
+
select.remove(1);
|
|
648
|
+
}
|
|
649
|
+
history.forEach(h => {
|
|
650
|
+
const opt = document.createElement('option');
|
|
651
|
+
opt.value = h.id;
|
|
652
|
+
const date = new Date(h.createdAt).toLocaleString('zh-TW');
|
|
653
|
+
opt.textContent = h.label + ' (' + date + ')';
|
|
654
|
+
select.appendChild(opt);
|
|
655
|
+
});
|
|
656
|
+
return history;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
Promise.all([projectsPromise, refreshHistory()]).then(function(results) {
|
|
660
|
+
var history = results[1] || [];
|
|
661
|
+
applyParamsFromUrl(history);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// 從歷史記錄載入
|
|
665
|
+
async function loadFromHistory() {
|
|
666
|
+
const historyId = document.getElementById('history').value;
|
|
667
|
+
if (!historyId) return;
|
|
668
|
+
await loadFromHistoryId(historyId);
|
|
669
|
+
// 重設歷史選單
|
|
670
|
+
document.getElementById('history').value = '';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function loadFromHistoryId(historyId) {
|
|
674
|
+
if (!historyId) return;
|
|
675
|
+
|
|
676
|
+
const result = document.getElementById('result');
|
|
677
|
+
const status = document.getElementById('status');
|
|
678
|
+
const svgContainer = document.getElementById('svgContainer');
|
|
679
|
+
const progressSection = document.querySelector('.progress-section');
|
|
680
|
+
|
|
681
|
+
result.style.display = 'block';
|
|
682
|
+
progressSection.style.display = 'none';
|
|
683
|
+
document.getElementById('logSection').innerHTML = '';
|
|
684
|
+
status.style.display = 'block';
|
|
685
|
+
status.className = 'status loading';
|
|
686
|
+
status.textContent = '載入中...';
|
|
687
|
+
status.style.background = '#fff3cd';
|
|
688
|
+
status.style.color = '#856404';
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
const res = await fetch('/api/history/' + historyId);
|
|
692
|
+
const data = await res.json();
|
|
693
|
+
|
|
694
|
+
if (!res.ok) {
|
|
695
|
+
throw new Error(data.error || '載入失敗');
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 更新表單與階段
|
|
699
|
+
setStage(
|
|
700
|
+
data.stage || 1,
|
|
701
|
+
data.nodeId ? { id: data.nodeId, label: data.nodeLabel || '', focus: data.nodeFocus || '' } : null
|
|
702
|
+
);
|
|
703
|
+
document.getElementById('project').value = data.project;
|
|
704
|
+
document.getElementById('functionPath').value = data.functionPath;
|
|
705
|
+
updatePreview();
|
|
706
|
+
|
|
707
|
+
// 顯示結果
|
|
708
|
+
status.className = 'status success';
|
|
709
|
+
status.textContent = '✅ 從歷史記錄載入 - 點擊圖上的方法名可跳轉到 IDEA';
|
|
710
|
+
|
|
711
|
+
// 載入可互動的 SVG
|
|
712
|
+
loadSvgInteractive(data.svgPath, data.project);
|
|
713
|
+
|
|
714
|
+
// 渲染 function 列表
|
|
715
|
+
if (data.ir && data.ir.nodes) {
|
|
716
|
+
renderFunctionList(data.ir.nodes, data.project);
|
|
717
|
+
renderRefineList(data.ir.nodes, data.project, data.functionPath);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
} catch (err) {
|
|
721
|
+
status.className = 'status error';
|
|
722
|
+
status.textContent = '❌ 錯誤: ' + err.message;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function updatePreview() {
|
|
729
|
+
const project = document.getElementById('project').value;
|
|
730
|
+
const fn = document.getElementById('functionPath').value;
|
|
731
|
+
const preview = document.getElementById('commandPreview');
|
|
732
|
+
|
|
733
|
+
if (!project || !fn) {
|
|
734
|
+
preview.textContent = '請選擇專案並輸入 function path';
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
var stageArg = ' --stage ' + (currentStage === 2 ? '2' : '1');
|
|
739
|
+
if (currentStage === 2 && currentNode) {
|
|
740
|
+
stageArg += ' --node ' + (currentNode.id || '');
|
|
741
|
+
if (currentNode.label) stageArg += ' --node-label "' + currentNode.label + '"';
|
|
742
|
+
if (currentNode.focus) stageArg += ' --node-focus "' + currentNode.focus + '"';
|
|
743
|
+
}
|
|
744
|
+
preview.textContent = 'apiflow5 --folder ' + WORKSPACE + ' \\\n --project ' + project + ' \\\n --path "' + fn + '" \\\n --out dist/flow.svg --keep' + stageArg;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function setStage(stage, node) {
|
|
748
|
+
currentStage = stage || 1;
|
|
749
|
+
currentNode = node || null;
|
|
750
|
+
var btn = document.getElementById('generateBtn');
|
|
751
|
+
if (btn) {
|
|
752
|
+
btn.textContent = currentStage === 2 ? '產生 Stage 2' : '產生 Stage 1';
|
|
753
|
+
}
|
|
754
|
+
var projectSelect = document.getElementById('project');
|
|
755
|
+
var functionInput = document.getElementById('functionPath');
|
|
756
|
+
if (projectSelect) projectSelect.disabled = currentStage === 2;
|
|
757
|
+
if (functionInput) functionInput.readOnly = currentStage === 2;
|
|
758
|
+
var radios = document.querySelectorAll('input[name="stageSelect"]');
|
|
759
|
+
radios.forEach(function(r) {
|
|
760
|
+
r.checked = String(currentStage) === r.value;
|
|
761
|
+
});
|
|
762
|
+
updatePreview();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
document.getElementById('project').addEventListener('change', updatePreview);
|
|
766
|
+
document.getElementById('functionPath').addEventListener('input', updatePreview);
|
|
767
|
+
|
|
768
|
+
function updateProgress(percent, message, stage) {
|
|
769
|
+
const bar = document.getElementById('progressBar');
|
|
770
|
+
const msg = document.getElementById('progressMessage');
|
|
771
|
+
const stages = document.querySelectorAll('#progressStages span');
|
|
772
|
+
|
|
773
|
+
bar.style.width = percent + '%';
|
|
774
|
+
bar.textContent = percent + '%';
|
|
775
|
+
msg.textContent = message;
|
|
776
|
+
|
|
777
|
+
stages.forEach(s => {
|
|
778
|
+
const stageNum = parseInt(s.dataset.stage);
|
|
779
|
+
s.classList.remove('active', 'done');
|
|
780
|
+
if (stageNum < stage) s.classList.add('done');
|
|
781
|
+
else if (stageNum === stage) s.classList.add('active');
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function addLog(message, isError = false) {
|
|
786
|
+
const logSection = document.getElementById('logSection');
|
|
787
|
+
const line = document.createElement('div');
|
|
788
|
+
line.className = 'log-line' + (isError ? ' error' : '');
|
|
789
|
+
line.textContent = message;
|
|
790
|
+
logSection.appendChild(line);
|
|
791
|
+
logSection.scrollTop = logSection.scrollHeight;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function resetUI() {
|
|
795
|
+
document.getElementById('logSection').innerHTML = '';
|
|
796
|
+
document.getElementById('status').style.display = 'none';
|
|
797
|
+
document.getElementById('svgContainer').innerHTML = '';
|
|
798
|
+
document.getElementById('zoomControls').style.display = 'none';
|
|
799
|
+
document.getElementById('functionList').style.display = 'none';
|
|
800
|
+
document.getElementById('functionListContent').innerHTML = '';
|
|
801
|
+
document.getElementById('refineList').style.display = 'none';
|
|
802
|
+
document.getElementById('refineListContent').innerHTML = '';
|
|
803
|
+
document.querySelector('.progress-section').style.display = 'block';
|
|
804
|
+
updateProgress(0, '準備中...', 0);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function showToast(message, duration) {
|
|
808
|
+
duration = duration || 3000;
|
|
809
|
+
var toast = document.getElementById('toast');
|
|
810
|
+
toast.textContent = message;
|
|
811
|
+
toast.classList.add('show');
|
|
812
|
+
setTimeout(function() {
|
|
813
|
+
toast.classList.remove('show');
|
|
814
|
+
}, duration);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function renderFunctionList(nodes, currentProject) {
|
|
818
|
+
var container = document.getElementById('functionListContent');
|
|
819
|
+
var listDiv = document.getElementById('functionList');
|
|
820
|
+
|
|
821
|
+
var items = nodes.filter(function(n) {
|
|
822
|
+
return n && n.type === 'call' && n.call && n.call.file;
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
if (items.length === 0) {
|
|
826
|
+
container.innerHTML = '<div class="function-list-empty">沒有可開啟的函數位置資訊</div>';
|
|
827
|
+
listDiv.style.display = 'block';
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// 去重複
|
|
832
|
+
var seen = {};
|
|
833
|
+
var uniqueItems = [];
|
|
834
|
+
items.forEach(function(n) {
|
|
835
|
+
var call = n.call || {};
|
|
836
|
+
var key = call.file + ':' + call.line;
|
|
837
|
+
if (!call.file || seen[key]) return;
|
|
838
|
+
seen[key] = true;
|
|
839
|
+
uniqueItems.push({
|
|
840
|
+
name: (call.name || n.label || '').split('#').pop().replace(/\\(.*\\)$/, ''),
|
|
841
|
+
fullName: call.name || n.label || '',
|
|
842
|
+
file: call.file,
|
|
843
|
+
line: call.line,
|
|
844
|
+
type: call.kind || 'method',
|
|
845
|
+
project: call.project
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
var html = '';
|
|
850
|
+
uniqueItems.forEach(function(item, idx) {
|
|
851
|
+
var location = item.file + (item.line ? ':' + item.line : '');
|
|
852
|
+
var canAnalyze = item.type === 'method' && item.fullName && item.fullName.includes('#');
|
|
853
|
+
html += '<div class="function-item">';
|
|
854
|
+
html += ' <span class="type-badge ' + item.type + '">' + item.type + '</span>';
|
|
855
|
+
html += ' <div class="info">';
|
|
856
|
+
html += ' <div class="name">' + item.name + '</div>';
|
|
857
|
+
html += ' <div class="location">' + location + '</div>';
|
|
858
|
+
html += ' </div>';
|
|
859
|
+
if (canAnalyze) {
|
|
860
|
+
html += ' <button class="open-btn explore-btn" data-idx="' + idx + '" onclick="analyzeFunctionDrillDown(' + idx + ')" style="margin-right:8px;">分析此函數</button>';
|
|
861
|
+
}
|
|
862
|
+
html += ' <button class="open-btn" data-idx="' + idx + '" onclick="openInIdea(' + idx + ')">開啟</button>';
|
|
863
|
+
html += '</div>';
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
container.innerHTML = html;
|
|
867
|
+
listDiv.style.display = 'block';
|
|
868
|
+
|
|
869
|
+
// 儲存到全域以便開啟時使用
|
|
870
|
+
window._functionItems = uniqueItems;
|
|
871
|
+
window._currentProject = currentProject;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function renderRefineList(nodes, currentProject, functionPath) {
|
|
875
|
+
var container = document.getElementById('refineListContent');
|
|
876
|
+
var listDiv = document.getElementById('refineList');
|
|
877
|
+
var items = (nodes || []).filter(function(n) {
|
|
878
|
+
return n && n.focus;
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
if (items.length === 0) {
|
|
882
|
+
container.innerHTML = '<div class="function-list-empty">沒有可細化的節點</div>';
|
|
883
|
+
listDiv.style.display = 'block';
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
var html = '';
|
|
888
|
+
items.forEach(function(n, idx) {
|
|
889
|
+
html += '<div class="function-item">';
|
|
890
|
+
html += ' <span class="type-badge method">focus</span>';
|
|
891
|
+
html += ' <div class="info">';
|
|
892
|
+
html += ' <div class="name">' + n.label + '</div>';
|
|
893
|
+
html += ' <div class="location">' + (n.focus || '') + '</div>';
|
|
894
|
+
html += ' </div>';
|
|
895
|
+
html += ' <button class="open-btn explore-btn" data-idx="' + idx + '" onclick="openRefine(' + idx + ')">細化</button>';
|
|
896
|
+
html += '</div>';
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
container.innerHTML = html;
|
|
900
|
+
listDiv.style.display = 'block';
|
|
901
|
+
window._refineItems = items;
|
|
902
|
+
window._currentProject = currentProject;
|
|
903
|
+
window._currentFunctionPath = functionPath;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function openInIdea(idx) {
|
|
907
|
+
var item = window._functionItems[idx];
|
|
908
|
+
if (!item) return;
|
|
909
|
+
|
|
910
|
+
var btn = document.querySelector('.open-btn[data-idx="' + idx + '"]');
|
|
911
|
+
btn.disabled = true;
|
|
912
|
+
btn.textContent = '開啟中...';
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
var res = await fetch('/api/open-idea', {
|
|
916
|
+
method: 'POST',
|
|
917
|
+
headers: { 'Content-Type': 'application/json' },
|
|
918
|
+
body: JSON.stringify({
|
|
919
|
+
file: item.file,
|
|
920
|
+
line: item.line,
|
|
921
|
+
project: item.project !== 'external' && item.project !== 'db' ? item.project : window._currentProject
|
|
922
|
+
})
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
var data = await res.json();
|
|
926
|
+
|
|
927
|
+
if (data.success) {
|
|
928
|
+
btn.textContent = '✓ 已開啟';
|
|
929
|
+
btn.classList.add('success');
|
|
930
|
+
showToast(data.message + ' - ' + item.file + (item.line ? ':' + item.line : ''));
|
|
931
|
+
} else {
|
|
932
|
+
btn.textContent = '開啟';
|
|
933
|
+
btn.disabled = false;
|
|
934
|
+
showToast('錯誤: ' + data.error);
|
|
935
|
+
}
|
|
936
|
+
} catch (err) {
|
|
937
|
+
btn.textContent = '開啟';
|
|
938
|
+
btn.disabled = false;
|
|
939
|
+
showToast('錯誤: ' + err.message);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
setTimeout(function() {
|
|
943
|
+
btn.disabled = false;
|
|
944
|
+
btn.textContent = '開啟';
|
|
945
|
+
btn.classList.remove('success');
|
|
946
|
+
}, 2000);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function openRefine(idx) {
|
|
950
|
+
var item = window._refineItems[idx];
|
|
951
|
+
if (!item) return;
|
|
952
|
+
|
|
953
|
+
var project = window._currentProject || '';
|
|
954
|
+
var functionPath = window._currentFunctionPath || '';
|
|
955
|
+
if (!project || !functionPath) {
|
|
956
|
+
showToast('缺少 project 或 function path');
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
var url = new URL(window.location.href);
|
|
961
|
+
url.searchParams.set('project', project);
|
|
962
|
+
url.searchParams.set('function', functionPath);
|
|
963
|
+
url.searchParams.set('stage', '2');
|
|
964
|
+
url.searchParams.set('node', item.id || '');
|
|
965
|
+
url.searchParams.set('label', item.label || '');
|
|
966
|
+
url.searchParams.set('focus', item.focus || '');
|
|
967
|
+
window.open(url.toString(), '_blank');
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
function analyzeFunctionDrillDown(idx) {
|
|
971
|
+
var item = window._functionItems[idx];
|
|
972
|
+
if (!item || !item.fullName) return;
|
|
973
|
+
|
|
974
|
+
var project = item.project || window._currentProject || '';
|
|
975
|
+
if (!project || project === 'external' || project === 'db') {
|
|
976
|
+
showToast('無法分析外部函數或資料庫查詢');
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// 開啟新分頁進行分析
|
|
981
|
+
var url = new URL(window.location.href);
|
|
982
|
+
url.searchParams.set('project', project);
|
|
983
|
+
url.searchParams.set('function', item.fullName);
|
|
984
|
+
url.searchParams.delete('stage');
|
|
985
|
+
url.searchParams.delete('node');
|
|
986
|
+
url.searchParams.delete('label');
|
|
987
|
+
url.searchParams.delete('focus');
|
|
988
|
+
window.open(url.toString(), '_blank');
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// 載入 SVG 並加入點擊互動
|
|
992
|
+
async function loadSvgInteractive(svgPath, currentProject) {
|
|
993
|
+
var svgContainer = document.getElementById('svgContainer');
|
|
994
|
+
var zoomControls = document.getElementById('zoomControls');
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
// 取得 SVG 內容
|
|
998
|
+
var res = await fetch('/svg?path=' + encodeURIComponent(svgPath));
|
|
999
|
+
var svgText = await res.text();
|
|
1000
|
+
|
|
1001
|
+
// 內嵌 SVG
|
|
1002
|
+
svgContainer.innerHTML = svgText;
|
|
1003
|
+
zoomControls.style.display = 'flex';
|
|
1004
|
+
applyZoom(1);
|
|
1005
|
+
|
|
1006
|
+
// 加入點擊事件監聽
|
|
1007
|
+
var svg = svgContainer.querySelector('svg');
|
|
1008
|
+
if (svg) {
|
|
1009
|
+
svg.style.transformOrigin = '0 0';
|
|
1010
|
+
svg.addEventListener('click', function(e) {
|
|
1011
|
+
var target = e.target;
|
|
1012
|
+
// 檢查是否點擊了可點擊的標籤
|
|
1013
|
+
var el = target.closest('.clickable-node') || target.closest('[data-file]');
|
|
1014
|
+
if (el) {
|
|
1015
|
+
var file = el.getAttribute('data-file');
|
|
1016
|
+
var line = el.getAttribute('data-line');
|
|
1017
|
+
var project = el.getAttribute('data-project') || currentProject;
|
|
1018
|
+
|
|
1019
|
+
if (file) {
|
|
1020
|
+
openInIdeaDirect(file, line, project);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// 加入 hover 提示
|
|
1026
|
+
svg.querySelectorAll('.clickable-node').forEach(function(el) {
|
|
1027
|
+
var file = el.getAttribute('data-file');
|
|
1028
|
+
var line = el.getAttribute('data-line');
|
|
1029
|
+
if (file) {
|
|
1030
|
+
el.style.cursor = 'pointer';
|
|
1031
|
+
el.setAttribute('title', '點擊開啟: ' + file + (line ? ':' + line : ''));
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
svgContainer.innerHTML = '<div style="color:red;">載入 SVG 失敗: ' + err.message + '</div>';
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function applyParamsFromUrl(history) {
|
|
1041
|
+
var params = new URLSearchParams(window.location.search);
|
|
1042
|
+
var project = params.get('project');
|
|
1043
|
+
var functionPath = params.get('function');
|
|
1044
|
+
var stageParam = params.get('stage');
|
|
1045
|
+
var nodeId = params.get('node');
|
|
1046
|
+
var nodeLabel = params.get('label');
|
|
1047
|
+
var nodeFocus = params.get('focus');
|
|
1048
|
+
if (!project || !functionPath) return;
|
|
1049
|
+
|
|
1050
|
+
if (stageParam === '2' && nodeId) {
|
|
1051
|
+
setStage(2, { id: nodeId, label: nodeLabel || '', focus: nodeFocus || '' });
|
|
1052
|
+
} else {
|
|
1053
|
+
setStage(1, null);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
document.getElementById('project').value = project;
|
|
1057
|
+
document.getElementById('functionPath').value = functionPath;
|
|
1058
|
+
updatePreview();
|
|
1059
|
+
|
|
1060
|
+
var match = (history || []).find(function(h) {
|
|
1061
|
+
return h.project === project &&
|
|
1062
|
+
h.functionPath === functionPath &&
|
|
1063
|
+
String(h.stage || 1) === String(currentStage || 1) &&
|
|
1064
|
+
String(h.nodeId || '') === String(nodeId || '');
|
|
1065
|
+
});
|
|
1066
|
+
if (match && match.id) {
|
|
1067
|
+
loadFromHistoryId(match.id);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
var currentZoom = 1;
|
|
1072
|
+
function applyZoom(value) {
|
|
1073
|
+
currentZoom = Math.max(0.5, Math.min(10, Number(value) || 1));
|
|
1074
|
+
var svg = document.querySelector('#svgContainer svg');
|
|
1075
|
+
if (svg) {
|
|
1076
|
+
svg.style.transform = 'scale(' + currentZoom + ')';
|
|
1077
|
+
}
|
|
1078
|
+
var zoomRange = document.getElementById('zoomRange');
|
|
1079
|
+
var zoomLabel = document.getElementById('zoomLabel');
|
|
1080
|
+
if (zoomRange) zoomRange.value = currentZoom;
|
|
1081
|
+
if (zoomLabel) zoomLabel.textContent = Math.round(currentZoom * 100) + '%';
|
|
1082
|
+
}
|
|
1083
|
+
function zoomIn() { applyZoom(currentZoom + 0.1); }
|
|
1084
|
+
function zoomOut() { applyZoom(currentZoom - 0.1); }
|
|
1085
|
+
function zoomReset() { applyZoom(1); }
|
|
1086
|
+
function zoomTo(v) { applyZoom(v); }
|
|
1087
|
+
|
|
1088
|
+
// 直接開啟 IDEA(給 SVG 點擊用)
|
|
1089
|
+
async function openInIdeaDirect(file, line, project) {
|
|
1090
|
+
showToast('正在開啟 ' + file + (line ? ':' + line : '') + '...');
|
|
1091
|
+
|
|
1092
|
+
try {
|
|
1093
|
+
var res = await fetch('/api/open-idea', {
|
|
1094
|
+
method: 'POST',
|
|
1095
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1096
|
+
body: JSON.stringify({ file: file, line: line, project: project })
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
var data = await res.json();
|
|
1100
|
+
if (data.success) {
|
|
1101
|
+
showToast(data.message + ' - ' + file + (line ? ':' + line : ''));
|
|
1102
|
+
} else {
|
|
1103
|
+
showToast('錯誤: ' + data.error);
|
|
1104
|
+
}
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
showToast('錯誤: ' + err.message);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function generate(force) {
|
|
1111
|
+
const project = document.getElementById('project').value;
|
|
1112
|
+
const functionPath = document.getElementById('functionPath').value;
|
|
1113
|
+
|
|
1114
|
+
if (!project) return alert('請選擇專案');
|
|
1115
|
+
if (!functionPath) return alert('請輸入 function path');
|
|
1116
|
+
if (currentStage === 2 && (!currentNode || !currentNode.id)) {
|
|
1117
|
+
return alert('請先選擇要細化的節點');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const btn = document.getElementById('generateBtn');
|
|
1121
|
+
const result = document.getElementById('result');
|
|
1122
|
+
const status = document.getElementById('status');
|
|
1123
|
+
const svgContainer = document.getElementById('svgContainer');
|
|
1124
|
+
|
|
1125
|
+
btn.disabled = true;
|
|
1126
|
+
btn.textContent = currentStage === 2 ? '細化中...' : '產生中...';
|
|
1127
|
+
result.style.display = 'block';
|
|
1128
|
+
resetUI();
|
|
1129
|
+
|
|
1130
|
+
// 使用 SSE 接收進度
|
|
1131
|
+
const params = new URLSearchParams({ project, functionPath, stage: String(currentStage) });
|
|
1132
|
+
if (currentStage === 2 && currentNode) {
|
|
1133
|
+
params.set('node', currentNode.id || '');
|
|
1134
|
+
params.set('label', currentNode.label || '');
|
|
1135
|
+
params.set('focus', currentNode.focus || '');
|
|
1136
|
+
}
|
|
1137
|
+
const evtSource = new EventSource('/api/generate?' + params.toString());
|
|
1138
|
+
|
|
1139
|
+
evtSource.addEventListener('progress', (e) => {
|
|
1140
|
+
const data = JSON.parse(e.data);
|
|
1141
|
+
updateProgress(data.percent, data.message, data.stage);
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
evtSource.addEventListener('log', (e) => {
|
|
1145
|
+
const data = JSON.parse(e.data);
|
|
1146
|
+
addLog(data.message);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
evtSource.addEventListener('done', (e) => {
|
|
1150
|
+
const data = JSON.parse(e.data);
|
|
1151
|
+
evtSource.close();
|
|
1152
|
+
|
|
1153
|
+
status.style.display = 'block';
|
|
1154
|
+
status.className = 'status success';
|
|
1155
|
+
status.textContent = currentStage === 2
|
|
1156
|
+
? '✅ 細化完成!點擊圖上的方法名可跳轉到 IDEA'
|
|
1157
|
+
: '✅ 產生成功!點擊圖上的方法名可跳轉到 IDEA';
|
|
1158
|
+
|
|
1159
|
+
// 載入可互動的 SVG
|
|
1160
|
+
loadSvgInteractive(data.svgPath, project);
|
|
1161
|
+
|
|
1162
|
+
// 渲染 function 列表
|
|
1163
|
+
if (data.ir && data.ir.nodes) {
|
|
1164
|
+
renderFunctionList(data.ir.nodes, project);
|
|
1165
|
+
renderRefineList(data.ir.nodes, project, functionPath);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// 刷新歷史記錄列表
|
|
1169
|
+
refreshHistory();
|
|
1170
|
+
|
|
1171
|
+
btn.disabled = false;
|
|
1172
|
+
btn.textContent = currentStage === 2 ? '產生 Stage 2' : '產生 Stage 1';
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
evtSource.addEventListener('error', (e) => {
|
|
1176
|
+
let errorMsg = '未知錯誤';
|
|
1177
|
+
try {
|
|
1178
|
+
const data = JSON.parse(e.data);
|
|
1179
|
+
errorMsg = data.message;
|
|
1180
|
+
} catch {}
|
|
1181
|
+
|
|
1182
|
+
evtSource.close();
|
|
1183
|
+
|
|
1184
|
+
status.style.display = 'block';
|
|
1185
|
+
status.className = 'status error';
|
|
1186
|
+
status.textContent = '❌ 錯誤: ' + errorMsg;
|
|
1187
|
+
addLog(errorMsg, true);
|
|
1188
|
+
|
|
1189
|
+
btn.disabled = false;
|
|
1190
|
+
btn.textContent = currentStage === 2 ? '產生 Stage 2' : '產生 Stage 1';
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
evtSource.onerror = () => {
|
|
1194
|
+
evtSource.close();
|
|
1195
|
+
btn.disabled = false;
|
|
1196
|
+
btn.textContent = currentStage === 2 ? '產生 Stage 2' : '產生 Stage 1';
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
</script>
|
|
1200
|
+
</body>
|
|
1201
|
+
</html>`;
|
|
1202
|
+
|
|
1203
|
+
// HTTP Server
|
|
1204
|
+
const server = createServer(async (req, res) => {
|
|
1205
|
+
const url = new URL(req.url, "http://localhost:" + PORT);
|
|
1206
|
+
|
|
1207
|
+
// CORS
|
|
1208
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1209
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1210
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1211
|
+
|
|
1212
|
+
if (req.method === "OPTIONS") {
|
|
1213
|
+
res.writeHead(204);
|
|
1214
|
+
return res.end();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Routes
|
|
1218
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1219
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1220
|
+
return res.end(HTML_PAGE);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (url.pathname === "/api/projects") {
|
|
1224
|
+
const projects = scanProjects(WORKSPACE);
|
|
1225
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1226
|
+
return res.end(JSON.stringify(projects));
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// 取得歷史記錄列表
|
|
1230
|
+
if (url.pathname === "/api/history" && req.method === "GET") {
|
|
1231
|
+
const history = loadHistory();
|
|
1232
|
+
// 回傳精簡版(不含完整 IR)
|
|
1233
|
+
const summary = history.map(h => ({
|
|
1234
|
+
id: h.id,
|
|
1235
|
+
createdAt: h.createdAt,
|
|
1236
|
+
project: h.project,
|
|
1237
|
+
functionPath: h.functionPath,
|
|
1238
|
+
stage: h.stage || 1,
|
|
1239
|
+
nodeId: h.nodeId || null,
|
|
1240
|
+
nodeLabel: h.nodeLabel || null,
|
|
1241
|
+
label: (h.stage || 1) === 2
|
|
1242
|
+
? `${h.project} / ${h.functionPath.split("#").pop()} / Stage2:${h.nodeLabel || h.nodeId || ""}`
|
|
1243
|
+
: `${h.project} / ${h.functionPath.split("#").pop()}`
|
|
1244
|
+
}));
|
|
1245
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1246
|
+
return res.end(JSON.stringify(summary));
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// 載入特定歷史記錄
|
|
1250
|
+
if (url.pathname.startsWith("/api/history/") && req.method === "GET") {
|
|
1251
|
+
const id = url.pathname.replace("/api/history/", "");
|
|
1252
|
+
const history = loadHistory();
|
|
1253
|
+
const entry = history.find(h => h.id === id);
|
|
1254
|
+
|
|
1255
|
+
if (!entry) {
|
|
1256
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1257
|
+
return res.end(JSON.stringify({ error: "找不到歷史記錄" }));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1261
|
+
return res.end(JSON.stringify(entry));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// SSE endpoint for generate
|
|
1265
|
+
if (url.pathname === "/api/generate") {
|
|
1266
|
+
const project = url.searchParams.get("project");
|
|
1267
|
+
const functionPath = url.searchParams.get("functionPath");
|
|
1268
|
+
const stage = Number(url.searchParams.get("stage") || "1");
|
|
1269
|
+
const nodeId = url.searchParams.get("node");
|
|
1270
|
+
const nodeLabel = url.searchParams.get("label");
|
|
1271
|
+
const nodeFocus = url.searchParams.get("focus");
|
|
1272
|
+
|
|
1273
|
+
if (!project || !functionPath) {
|
|
1274
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1275
|
+
return res.end(JSON.stringify({ error: "缺少 project 或 functionPath" }));
|
|
1276
|
+
}
|
|
1277
|
+
if (stage === 2 && !nodeId) {
|
|
1278
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1279
|
+
return res.end(JSON.stringify({ error: "缺少 node 參數" }));
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// SSE headers
|
|
1283
|
+
res.writeHead(200, {
|
|
1284
|
+
"Content-Type": "text/event-stream",
|
|
1285
|
+
"Cache-Control": "no-cache",
|
|
1286
|
+
"Connection": "keep-alive"
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
console.log("[Generate] project=" + project + ", function=" + functionPath);
|
|
1290
|
+
|
|
1291
|
+
runApiflowWithProgress(res, {
|
|
1292
|
+
project,
|
|
1293
|
+
functionPath,
|
|
1294
|
+
workspace: WORKSPACE,
|
|
1295
|
+
stage,
|
|
1296
|
+
nodeId,
|
|
1297
|
+
nodeLabel,
|
|
1298
|
+
nodeFocus
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// 在 IDEA 中開啟檔案
|
|
1305
|
+
if (url.pathname === "/api/open-idea" && req.method === "POST") {
|
|
1306
|
+
let body = "";
|
|
1307
|
+
req.on("data", (chunk) => (body += chunk));
|
|
1308
|
+
req.on("end", async () => {
|
|
1309
|
+
try {
|
|
1310
|
+
const { file, line, project } = JSON.parse(body);
|
|
1311
|
+
|
|
1312
|
+
if (!file) {
|
|
1313
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1314
|
+
return res.end(JSON.stringify({ error: "缺少 file 參數" }));
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// 組合完整路徑(避免 project 重複)
|
|
1318
|
+
let fullPath = file;
|
|
1319
|
+
if (!path.isAbsolute(file)) {
|
|
1320
|
+
if (project && file.startsWith(project + path.sep)) {
|
|
1321
|
+
fullPath = path.join(WORKSPACE, file);
|
|
1322
|
+
} else if (project) {
|
|
1323
|
+
fullPath = path.join(WORKSPACE, project, file);
|
|
1324
|
+
} else {
|
|
1325
|
+
fullPath = path.join(WORKSPACE, file);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (!existsSync(fullPath)) {
|
|
1330
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1331
|
+
return res.end(JSON.stringify({ error: "檔案不存在: " + fullPath }));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
console.log("[OpenIDEA] " + fullPath + (line ? ":" + line : ""));
|
|
1335
|
+
|
|
1336
|
+
const result = await openInIdea(fullPath, line);
|
|
1337
|
+
|
|
1338
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1339
|
+
res.end(JSON.stringify({
|
|
1340
|
+
success: true,
|
|
1341
|
+
wasRunning: result.wasRunning,
|
|
1342
|
+
message: result.wasRunning
|
|
1343
|
+
? "已在 IDEA 中開啟"
|
|
1344
|
+
: "正在啟動 IDEA..."
|
|
1345
|
+
}));
|
|
1346
|
+
} catch (err) {
|
|
1347
|
+
console.error("[OpenIDEA Error]", err.message);
|
|
1348
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1349
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (url.pathname === "/svg") {
|
|
1356
|
+
const svgPath = url.searchParams.get("path");
|
|
1357
|
+
if (svgPath && existsSync(svgPath)) {
|
|
1358
|
+
const svg = readFileSync(svgPath);
|
|
1359
|
+
res.writeHead(200, { "Content-Type": "image/svg+xml" });
|
|
1360
|
+
return res.end(svg);
|
|
1361
|
+
}
|
|
1362
|
+
res.writeHead(404);
|
|
1363
|
+
return res.end("SVG not found");
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// 404
|
|
1367
|
+
res.writeHead(404);
|
|
1368
|
+
res.end("Not Found");
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
server.listen(PORT, () => {
|
|
1372
|
+
console.log([
|
|
1373
|
+
"",
|
|
1374
|
+
"╔════════════════════════════════════════════╗",
|
|
1375
|
+
"║ API Flow (Staged) Generator GUI ║",
|
|
1376
|
+
"║ http://localhost:" + PORT + " ║",
|
|
1377
|
+
"╚════════════════════════════════════════════╝",
|
|
1378
|
+
"",
|
|
1379
|
+
"Workspace: " + WORKSPACE,
|
|
1380
|
+
"按 Ctrl+C 結束",
|
|
1381
|
+
""
|
|
1382
|
+
].join("\n"));
|
|
1383
|
+
});
|