qwenproxy-cli 1.0.0 → 1.0.1
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/LICENSE +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +78 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2800 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +264 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
|
@@ -1,823 +1,1018 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QwenProxy TUI - Interactive Chat Tester View (Tab 2)
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
|
-
import type { KeyEvent } from "../screen.ts";
|
|
7
|
-
import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapContentLine } from "../theme.ts";
|
|
8
|
-
import { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
|
|
9
|
-
import { ServerManager } from "../server-manager.ts";
|
|
10
|
-
import { formatMarkdown, formatReasoning } from "../markdown.ts";
|
|
11
|
-
|
|
12
|
-
interface ChatMessage {
|
|
13
|
-
role: "user" | "assistant";
|
|
14
|
-
content: string;
|
|
15
|
-
reasoning?: string;
|
|
16
|
-
model?: string;
|
|
17
|
-
ttfbMs?: number;
|
|
18
|
-
totalTimeMs?: number;
|
|
19
|
-
cachedContentLines?: string[];
|
|
20
|
-
cachedReasoningBox?: string[];
|
|
21
|
-
cachedWidth?: number;
|
|
22
|
-
}
|
|
23
|
-
export function classifyModel(modelId: string): { badge: string; category: string } {
|
|
24
|
-
const lower = modelId.toLowerCase();
|
|
25
|
-
if (
|
|
26
|
-
lower.includes("image") ||
|
|
27
|
-
lower.startsWith("z-image") ||
|
|
28
|
-
lower.includes("t2i") ||
|
|
29
|
-
lower.includes("i2i")
|
|
30
|
-
) {
|
|
31
|
-
return { badge: theme.lavender("[Imagem]"), category: "Geração de Imagem" };
|
|
32
|
-
}
|
|
33
|
-
if (lower.includes("video") || lower.includes("t2v") || lower.includes("i2v")) {
|
|
34
|
-
return { badge: theme.peach("[Vídeo] "), category: "Geração de Vídeo" };
|
|
35
|
-
}
|
|
36
|
-
return { badge: theme.cyan("[Texto] "), category: "Texto & Raciocínio" };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class ChatView implements TuiView {
|
|
40
|
-
public readonly id = "chat";
|
|
41
|
-
public readonly title = "Chat";
|
|
42
|
-
public readonly tabNumber = 2;
|
|
43
|
-
|
|
44
|
-
private availableModels = [
|
|
45
|
-
"qwen3.8-max",
|
|
46
|
-
"qwen3.7-plus",
|
|
47
|
-
"qwen3.7-max",
|
|
48
|
-
"z-image-turbo",
|
|
49
|
-
"qwen-image-3.0-pro",
|
|
50
|
-
"wan3.0-video",
|
|
51
|
-
];
|
|
52
|
-
private selectedModelIndex = 0;
|
|
53
|
-
private messages: ChatMessage[] = [];
|
|
54
|
-
private inputBuffer = "";
|
|
55
|
-
private cursorPos = 0;
|
|
56
|
-
private scrollOffset = 0; // 0 = follow bottom (newest messages)
|
|
57
|
-
private
|
|
58
|
-
private
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (key.name === "
|
|
188
|
-
const { row } = key.mouse;
|
|
189
|
-
if (row >= 9 && row < 9 + this.availableModels.length) {
|
|
190
|
-
const
|
|
191
|
-
this.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
if (key.name === "
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (key.name === "
|
|
236
|
-
const { row } = key.mouse;
|
|
237
|
-
if (row >= 9 && row < 9 + this.availableEfforts.length) {
|
|
238
|
-
|
|
239
|
-
this.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
(key.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
this.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
this.
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
this.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (key.name === "
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (key.name === "
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
this.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
:
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
?
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* QwenProxy TUI - Interactive Chat Tester View (Tab 2)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
|
+
import type { KeyEvent } from "../screen.ts";
|
|
7
|
+
import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapContentLine } from "../theme.ts";
|
|
8
|
+
import { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
|
|
9
|
+
import { ServerManager } from "../server-manager.ts";
|
|
10
|
+
import { formatMarkdown, formatReasoning } from "../markdown.ts";
|
|
11
|
+
|
|
12
|
+
interface ChatMessage {
|
|
13
|
+
role: "user" | "assistant";
|
|
14
|
+
content: string;
|
|
15
|
+
reasoning?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
ttfbMs?: number;
|
|
18
|
+
totalTimeMs?: number;
|
|
19
|
+
cachedContentLines?: string[];
|
|
20
|
+
cachedReasoningBox?: string[];
|
|
21
|
+
cachedWidth?: number;
|
|
22
|
+
}
|
|
23
|
+
export function classifyModel(modelId: string): { badge: string; category: string } {
|
|
24
|
+
const lower = modelId.toLowerCase();
|
|
25
|
+
if (
|
|
26
|
+
lower.includes("image") ||
|
|
27
|
+
lower.startsWith("z-image") ||
|
|
28
|
+
lower.includes("t2i") ||
|
|
29
|
+
lower.includes("i2i")
|
|
30
|
+
) {
|
|
31
|
+
return { badge: theme.lavender("[Imagem]"), category: "Geração de Imagem" };
|
|
32
|
+
}
|
|
33
|
+
if (lower.includes("video") || lower.includes("t2v") || lower.includes("i2v")) {
|
|
34
|
+
return { badge: theme.peach("[Vídeo] "), category: "Geração de Vídeo" };
|
|
35
|
+
}
|
|
36
|
+
return { badge: theme.cyan("[Texto] "), category: "Texto & Raciocínio" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class ChatView implements TuiView {
|
|
40
|
+
public readonly id = "chat";
|
|
41
|
+
public readonly title = "Chat";
|
|
42
|
+
public readonly tabNumber = 2;
|
|
43
|
+
|
|
44
|
+
private availableModels = [
|
|
45
|
+
"qwen3.8-max",
|
|
46
|
+
"qwen3.7-plus",
|
|
47
|
+
"qwen3.7-max",
|
|
48
|
+
"z-image-turbo",
|
|
49
|
+
"qwen-image-3.0-pro",
|
|
50
|
+
"wan3.0-video",
|
|
51
|
+
];
|
|
52
|
+
private selectedModelIndex = 0;
|
|
53
|
+
private messages: ChatMessage[] = [];
|
|
54
|
+
private inputBuffer = "";
|
|
55
|
+
private cursorPos = 0;
|
|
56
|
+
private scrollOffset = 0; // 0 = follow bottom (newest messages)
|
|
57
|
+
private lastWidth = 80;
|
|
58
|
+
private lastHeight = 24;
|
|
59
|
+
private lastMaxOffset = 0;
|
|
60
|
+
private lastVisibleCapacity = 0;
|
|
61
|
+
private hoveredHeaderBtn: "model" | "effort" | null = null;
|
|
62
|
+
private isScrollbarHovered = false;
|
|
63
|
+
private isDraggingScrollbar = false;
|
|
64
|
+
private modelBtnStartCol = 0;
|
|
65
|
+
private modelBtnEndCol = 0;
|
|
66
|
+
private effortBtnStartCol = 0;
|
|
67
|
+
private effortBtnEndCol = 0;
|
|
68
|
+
private isModelModalOpen = false;
|
|
69
|
+
private modalSelectedIndex = 0;
|
|
70
|
+
private availableEfforts: Array<{
|
|
71
|
+
id: "high" | "medium" | "low";
|
|
72
|
+
label: string;
|
|
73
|
+
desc: string;
|
|
74
|
+
badge: string;
|
|
75
|
+
}> = [
|
|
76
|
+
{
|
|
77
|
+
id: "high",
|
|
78
|
+
label: "High (Thinking)",
|
|
79
|
+
desc: "Raciocínio profundo ativado (ideal para código)",
|
|
80
|
+
badge: theme.green("[High]"),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "medium",
|
|
84
|
+
label: "Medium (Auto)",
|
|
85
|
+
desc: "Raciocínio dinâmico (Qwen decide quando pensar)",
|
|
86
|
+
badge: theme.yellow("[Medium]"),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "low",
|
|
90
|
+
label: "Low (Fast)",
|
|
91
|
+
desc: "Raciocínio desativado (respostas ultrarrápidas)",
|
|
92
|
+
badge: theme.cyan("[Low]"),
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
private selectedEffort: "high" | "medium" | "low" = "high";
|
|
96
|
+
private isEffortModalOpen = false;
|
|
97
|
+
private effortSelectedIndex = 0;
|
|
98
|
+
private isGenerating = false;
|
|
99
|
+
private currentAbortController: AbortController | null = null;
|
|
100
|
+
private statusNote = "";
|
|
101
|
+
private lastSnapshot: ProxyStatusSnapshot | null = null;
|
|
102
|
+
private onNeedsRender?: () => void;
|
|
103
|
+
private renderThrottleTimer: NodeJS.Timeout | null = null;
|
|
104
|
+
private spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
105
|
+
private spinnerIndex = 0;
|
|
106
|
+
private spinnerInterval: NodeJS.Timeout | null = null;
|
|
107
|
+
|
|
108
|
+
private scheduleRender(): void {
|
|
109
|
+
if (this.renderThrottleTimer) return;
|
|
110
|
+
this.renderThrottleTimer = setTimeout(() => {
|
|
111
|
+
this.renderThrottleTimer = null;
|
|
112
|
+
this.onNeedsRender?.();
|
|
113
|
+
}, 40);
|
|
114
|
+
}
|
|
115
|
+
private flushRender(): void {
|
|
116
|
+
if (this.renderThrottleTimer) {
|
|
117
|
+
clearTimeout(this.renderThrottleTimer);
|
|
118
|
+
this.renderThrottleTimer = null;
|
|
119
|
+
}
|
|
120
|
+
this.onNeedsRender?.();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
constructor(onNeedsRender?: () => void) {
|
|
124
|
+
this.onNeedsRender = onNeedsRender;
|
|
125
|
+
void this.refreshModels();
|
|
126
|
+
}
|
|
127
|
+
public onActivate(): void {
|
|
128
|
+
void this.refreshModels();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public isModalOpen(): boolean {
|
|
132
|
+
return this.isModelModalOpen || this.isEffortModalOpen;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public async refreshModels(): Promise<void> {
|
|
136
|
+
try {
|
|
137
|
+
const live = await fetchLiveModels();
|
|
138
|
+
if (live.length > 0) {
|
|
139
|
+
const current = this.availableModels[this.selectedModelIndex];
|
|
140
|
+
this.availableModels = live;
|
|
141
|
+
const foundIdx = this.availableModels.indexOf(current);
|
|
142
|
+
this.selectedModelIndex = foundIdx !== -1 ? foundIdx : 0;
|
|
143
|
+
this.onNeedsRender?.();
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
public getShortcuts(): Array<{ key: string; label: string }> {
|
|
148
|
+
if (this.isModelModalOpen) {
|
|
149
|
+
return [
|
|
150
|
+
{ key: "Enter", label: `${glyphs.enter} Escolher` },
|
|
151
|
+
{ key: "Esc", label: `${glyphs.cross} Fechar` },
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
if (this.isEffortModalOpen) {
|
|
155
|
+
return [
|
|
156
|
+
{ key: "Enter", label: `${glyphs.enter} Confirmar` },
|
|
157
|
+
{ key: "Esc", label: `${glyphs.cross} Manter` },
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
{ key: "Enter", label: `${glyphs.enter} Enviar` },
|
|
162
|
+
{ key: "Esc", label: `${glyphs.cross} Parar` },
|
|
163
|
+
{ key: "Ctrl+L", label: `${glyphs.broom} Limpar` },
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private applyChosenModel(idx: number): void {
|
|
168
|
+
const chosen = this.availableModels[idx];
|
|
169
|
+
if (!chosen) return;
|
|
170
|
+
this.selectedModelIndex = idx;
|
|
171
|
+
this.isModelModalOpen = false;
|
|
172
|
+
|
|
173
|
+
const info = classifyModel(chosen);
|
|
174
|
+
if (info.category === "Texto & Raciocínio") {
|
|
175
|
+
this.isEffortModalOpen = true;
|
|
176
|
+
const effIdx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
177
|
+
this.effortSelectedIndex = effIdx !== -1 ? effIdx : 0;
|
|
178
|
+
this.statusNote = `Modelo ${chosen} escolhido. Escolha o esforço de raciocínio (Effort):`;
|
|
179
|
+
} else {
|
|
180
|
+
this.statusNote = `Modelo alterado para ${chosen}`;
|
|
181
|
+
}
|
|
182
|
+
this.onNeedsRender?.();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
186
|
+
if (this.isModelModalOpen) {
|
|
187
|
+
if (key.name === "hover" && key.mouse) {
|
|
188
|
+
const { row } = key.mouse;
|
|
189
|
+
if (row >= 9 && row < 9 + this.availableModels.length) {
|
|
190
|
+
const hoverIdx = row - 9;
|
|
191
|
+
if (this.modalSelectedIndex !== hoverIdx) {
|
|
192
|
+
this.modalSelectedIndex = hoverIdx;
|
|
193
|
+
this.onNeedsRender?.();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (key.name === "click" && key.mouse) {
|
|
199
|
+
const { row } = key.mouse;
|
|
200
|
+
if (row >= 9 && row < 9 + this.availableModels.length) {
|
|
201
|
+
const chosenIdx = row - 9;
|
|
202
|
+
this.applyChosenModel(chosenIdx);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
this.isModelModalOpen = false;
|
|
206
|
+
this.onNeedsRender?.();
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
|
|
210
|
+
this.modalSelectedIndex = Math.max(0, this.modalSelectedIndex - 1);
|
|
211
|
+
this.onNeedsRender?.();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (key.name === "down" || key.name === "wheeldown" || (key.name === "j" && !key.ctrl)) {
|
|
215
|
+
this.modalSelectedIndex = Math.min(
|
|
216
|
+
this.availableModels.length - 1,
|
|
217
|
+
this.modalSelectedIndex + 1,
|
|
218
|
+
);
|
|
219
|
+
this.onNeedsRender?.();
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
if (key.name === "return") {
|
|
223
|
+
this.applyChosenModel(this.modalSelectedIndex);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (key.name === "escape") {
|
|
227
|
+
this.isModelModalOpen = false;
|
|
228
|
+
this.onNeedsRender?.();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
// 2. Effort Selection Modal Active
|
|
234
|
+
if (this.isEffortModalOpen) {
|
|
235
|
+
if (key.name === "hover" && key.mouse) {
|
|
236
|
+
const { row } = key.mouse;
|
|
237
|
+
if (row >= 9 && row < 9 + this.availableEfforts.length) {
|
|
238
|
+
const hoverIdx = row - 9;
|
|
239
|
+
if (this.effortSelectedIndex !== hoverIdx) {
|
|
240
|
+
this.effortSelectedIndex = hoverIdx;
|
|
241
|
+
this.onNeedsRender?.();
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (key.name === "click" && key.mouse) {
|
|
247
|
+
const { row } = key.mouse;
|
|
248
|
+
if (row >= 9 && row < 9 + this.availableEfforts.length) {
|
|
249
|
+
this.selectedEffort = this.availableEfforts[row - 9].id;
|
|
250
|
+
this.isEffortModalOpen = false;
|
|
251
|
+
const currentM = this.availableModels[this.selectedModelIndex];
|
|
252
|
+
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[row - 9].label}`;
|
|
253
|
+
this.onNeedsRender?.();
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
this.isEffortModalOpen = false;
|
|
257
|
+
this.onNeedsRender?.();
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
|
|
261
|
+
this.effortSelectedIndex = Math.max(0, this.effortSelectedIndex - 1);
|
|
262
|
+
this.onNeedsRender?.();
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
if (key.name === "down" || key.name === "wheeldown" || (key.name === "j" && !key.ctrl)) {
|
|
266
|
+
this.effortSelectedIndex = Math.min(
|
|
267
|
+
this.availableEfforts.length - 1,
|
|
268
|
+
this.effortSelectedIndex + 1,
|
|
269
|
+
);
|
|
270
|
+
this.onNeedsRender?.();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
if (key.name === "return") {
|
|
274
|
+
this.selectedEffort = this.availableEfforts[this.effortSelectedIndex].id;
|
|
275
|
+
this.isEffortModalOpen = false;
|
|
276
|
+
const currentM = this.availableModels[this.selectedModelIndex];
|
|
277
|
+
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[this.effortSelectedIndex].label}`;
|
|
278
|
+
this.onNeedsRender?.();
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (key.name === "escape") {
|
|
282
|
+
this.isEffortModalOpen = false;
|
|
283
|
+
this.onNeedsRender?.();
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 3. Header button hover (rows 4 to 6: Model, Effort)
|
|
290
|
+
if (key.name === "hover" && key.mouse) {
|
|
291
|
+
const { row, col } = key.mouse;
|
|
292
|
+
if (row >= 4 && row <= 6 && !this.isModelModalOpen && !this.isEffortModalOpen) {
|
|
293
|
+
let target: "model" | "effort" | null = null;
|
|
294
|
+
if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
|
|
295
|
+
target = "model";
|
|
296
|
+
} else if (this.effortBtnStartCol > 0 && col >= this.effortBtnStartCol - 1 && col <= this.effortBtnEndCol + 1) {
|
|
297
|
+
target = "effort";
|
|
298
|
+
}
|
|
299
|
+
if (this.hoveredHeaderBtn !== target) {
|
|
300
|
+
this.hoveredHeaderBtn = target;
|
|
301
|
+
this.onNeedsRender?.();
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
} else if (this.hoveredHeaderBtn !== null) {
|
|
305
|
+
this.hoveredHeaderBtn = null;
|
|
306
|
+
this.onNeedsRender?.();
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 4. Header button click (rows 4 to 6: Model, Effort)
|
|
312
|
+
if (
|
|
313
|
+
key.name === "click" &&
|
|
314
|
+
key.mouse &&
|
|
315
|
+
key.mouse.row >= 4 &&
|
|
316
|
+
key.mouse.row <= 6 &&
|
|
317
|
+
!this.isModelModalOpen &&
|
|
318
|
+
!this.isEffortModalOpen
|
|
319
|
+
) {
|
|
320
|
+
const col = key.mouse.col;
|
|
321
|
+
if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
|
|
322
|
+
this.isModelModalOpen = true;
|
|
323
|
+
this.modalSelectedIndex = this.selectedModelIndex;
|
|
324
|
+
this.hoveredHeaderBtn = null;
|
|
325
|
+
this.onNeedsRender?.();
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (this.effortBtnStartCol > 0 && col >= this.effortBtnStartCol - 1 && col <= this.effortBtnEndCol + 1) {
|
|
329
|
+
this.isEffortModalOpen = true;
|
|
330
|
+
const idx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
331
|
+
this.effortSelectedIndex = idx !== -1 ? idx : 0;
|
|
332
|
+
this.hoveredHeaderBtn = null;
|
|
333
|
+
this.onNeedsRender?.();
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Keyboard shortcuts to open modals
|
|
339
|
+
if (
|
|
340
|
+
key.name === "f2" ||
|
|
341
|
+
(key.ctrl && key.name === "o") ||
|
|
342
|
+
(key.meta && key.name === "m")
|
|
343
|
+
) {
|
|
344
|
+
this.isModelModalOpen = true;
|
|
345
|
+
this.modalSelectedIndex = this.selectedModelIndex;
|
|
346
|
+
this.onNeedsRender?.();
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (key.name === "f3") {
|
|
351
|
+
const currentM = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
352
|
+
const info = classifyModel(currentM);
|
|
353
|
+
if (info.category === "Texto & Raciocínio") {
|
|
354
|
+
this.isEffortModalOpen = true;
|
|
355
|
+
const idx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
356
|
+
this.effortSelectedIndex = idx !== -1 ? idx : 0;
|
|
357
|
+
this.onNeedsRender?.();
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 5. Scrollbar hover, click & drag
|
|
363
|
+
const isMouseOnScrollbar = (col: number, row: number) => {
|
|
364
|
+
return (
|
|
365
|
+
col >= this.lastWidth - 2 &&
|
|
366
|
+
col <= this.lastWidth &&
|
|
367
|
+
row >= 8 &&
|
|
368
|
+
row <= 7 + this.lastVisibleCapacity
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
if (key.name === "hover" && key.mouse && !this.isModelModalOpen && !this.isEffortModalOpen) {
|
|
373
|
+
const onScrollbar = isMouseOnScrollbar(key.mouse.col, key.mouse.row);
|
|
374
|
+
if (this.isScrollbarHovered !== onScrollbar) {
|
|
375
|
+
this.isScrollbarHovered = onScrollbar;
|
|
376
|
+
this.onNeedsRender?.();
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (key.name === "click" && key.mouse && !this.isModelModalOpen && !this.isEffortModalOpen) {
|
|
382
|
+
if (isMouseOnScrollbar(key.mouse.col, key.mouse.row)) {
|
|
383
|
+
if (this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
384
|
+
this.isDraggingScrollbar = true;
|
|
385
|
+
const r = key.mouse.row - 8;
|
|
386
|
+
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
387
|
+
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
388
|
+
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
389
|
+
this.onNeedsRender?.();
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (key.name === "drag" && key.mouse && !this.isModelModalOpen && !this.isEffortModalOpen) {
|
|
396
|
+
if (this.isDraggingScrollbar && this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
397
|
+
const r = key.mouse.row - 8;
|
|
398
|
+
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
399
|
+
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
400
|
+
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
401
|
+
this.onNeedsRender?.();
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (key.name === "release") {
|
|
407
|
+
if (this.isDraggingScrollbar) {
|
|
408
|
+
this.isDraggingScrollbar = false;
|
|
409
|
+
this.onNeedsRender?.();
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 5. Chat history scrolling (Mouse wheel / PageUp / PageDown / Up / Down)
|
|
415
|
+
if (key.name === "wheelup") {
|
|
416
|
+
this.scrollOffset = this.lastMaxOffset > 0 ? Math.min(this.lastMaxOffset, this.scrollOffset + 2) : this.scrollOffset + 2;
|
|
417
|
+
this.onNeedsRender?.();
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (key.name === "wheeldown") {
|
|
421
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 2);
|
|
422
|
+
this.onNeedsRender?.();
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
if (key.name === "pageup") {
|
|
426
|
+
this.scrollOffset = this.lastMaxOffset > 0 ? Math.min(this.lastMaxOffset, this.scrollOffset + 6) : this.scrollOffset + 6;
|
|
427
|
+
this.onNeedsRender?.();
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
if (key.name === "pagedown") {
|
|
431
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 6);
|
|
432
|
+
this.onNeedsRender?.();
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if ((key.ctrl || key.shift) && key.name === "up") {
|
|
436
|
+
this.scrollOffset = this.lastMaxOffset > 0 ? Math.min(this.lastMaxOffset, this.scrollOffset + 2) : this.scrollOffset + 2;
|
|
437
|
+
this.onNeedsRender?.();
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if ((key.ctrl || key.shift) && key.name === "down") {
|
|
441
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 2);
|
|
442
|
+
this.onNeedsRender?.();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (!this.isGenerating && this.inputBuffer.length === 0 && key.name === "up") {
|
|
446
|
+
this.scrollOffset = this.lastMaxOffset > 0 ? Math.min(this.lastMaxOffset, this.scrollOffset + 1) : this.scrollOffset + 1;
|
|
447
|
+
this.onNeedsRender?.();
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
if (!this.isGenerating && this.inputBuffer.length === 0 && key.name === "down") {
|
|
451
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
452
|
+
this.onNeedsRender?.();
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
if (key.name === "home") {
|
|
456
|
+
this.scrollOffset = this.lastMaxOffset;
|
|
457
|
+
this.onNeedsRender?.();
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
if (key.name === "end") {
|
|
461
|
+
this.scrollOffset = 0;
|
|
462
|
+
this.onNeedsRender?.();
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 5. Cursor movement within input line using Left / Right
|
|
467
|
+
if (key.name === "left") {
|
|
468
|
+
this.cursorPos = Math.max(0, this.cursorPos - 1);
|
|
469
|
+
this.onNeedsRender?.();
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
if (key.name === "right") {
|
|
473
|
+
this.cursorPos = Math.min(this.inputBuffer.length, this.cursorPos + 1);
|
|
474
|
+
this.onNeedsRender?.();
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (key.name === "home") {
|
|
478
|
+
this.cursorPos = 0;
|
|
479
|
+
this.onNeedsRender?.();
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
if (key.name === "end") {
|
|
483
|
+
this.cursorPos = this.inputBuffer.length;
|
|
484
|
+
this.onNeedsRender?.();
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
// 5. Paste from clipboard with Ctrl+V
|
|
488
|
+
if (key.ctrl && (key.name === "v" || key.raw === "\x16")) {
|
|
489
|
+
const { getClipboardText } = require("../theme.ts");
|
|
490
|
+
const pasted = getClipboardText();
|
|
491
|
+
if (pasted) {
|
|
492
|
+
this.inputBuffer =
|
|
493
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
494
|
+
pasted +
|
|
495
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
496
|
+
this.cursorPos += pasted.length;
|
|
497
|
+
this.onNeedsRender?.();
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// 6. Clear history with Ctrl+L
|
|
503
|
+
if (key.ctrl && key.name === "l") {
|
|
504
|
+
this.messages = [];
|
|
505
|
+
this.statusNote = "Histórico do chat limpo.";
|
|
506
|
+
this.onNeedsRender?.();
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 6. Clear input line with single Ctrl+C (standard CLI behavior)
|
|
511
|
+
if (key.ctrl && key.name === "c" && this.inputBuffer.length > 0) {
|
|
512
|
+
this.inputBuffer = "";
|
|
513
|
+
this.cursorPos = 0;
|
|
514
|
+
this.onNeedsRender?.();
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 7. Backspace at cursor
|
|
519
|
+
if (key.name === "backspace") {
|
|
520
|
+
if (this.cursorPos > 0) {
|
|
521
|
+
this.inputBuffer =
|
|
522
|
+
this.inputBuffer.slice(0, this.cursorPos - 1) +
|
|
523
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
524
|
+
this.cursorPos--;
|
|
525
|
+
this.onNeedsRender?.();
|
|
526
|
+
}
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 7. Delete at cursor
|
|
531
|
+
if (key.name === "delete") {
|
|
532
|
+
if (this.cursorPos < this.inputBuffer.length) {
|
|
533
|
+
this.inputBuffer =
|
|
534
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
535
|
+
this.inputBuffer.slice(this.cursorPos + 1);
|
|
536
|
+
this.onNeedsRender?.();
|
|
537
|
+
}
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 8. Submit message on Enter
|
|
542
|
+
if (key.name === "return") {
|
|
543
|
+
const text = this.inputBuffer.trim();
|
|
544
|
+
if (!text || this.isGenerating) return true;
|
|
545
|
+
|
|
546
|
+
// Block submitting if there are 0 accounts configured
|
|
547
|
+
if (this.lastSnapshot && this.lastSnapshot.accounts.length === 0) {
|
|
548
|
+
this.statusNote = theme.yellow("[!] Adicione uma conta na aba [5] Contas antes de iniciar o chat.");
|
|
549
|
+
this.onNeedsRender?.();
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const serverState = ServerManager.getInstance().getState();
|
|
554
|
+
if (serverState === "warming") {
|
|
555
|
+
this.statusNote = theme.yellow("Aguarde: inicializando proxy...");
|
|
556
|
+
this.onNeedsRender?.();
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
if (serverState === "error") {
|
|
560
|
+
this.statusNote = theme.red("Servidor com erro. Verifique a aba Logs.");
|
|
561
|
+
this.onNeedsRender?.();
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
this.inputBuffer = "";
|
|
565
|
+
this.cursorPos = 0;
|
|
566
|
+
this.scrollOffset = 0;
|
|
567
|
+
this.sendMessage(text);
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 9. Ignore Tab key (handled globally for tab switching)
|
|
572
|
+
if (key.name === "tab") {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
// 10. Character typing (insert at cursor position)
|
|
576
|
+
if (key.char && !key.ctrl && !key.meta && key.name !== "tab") {
|
|
577
|
+
if (key.char >= " ") {
|
|
578
|
+
this.inputBuffer =
|
|
579
|
+
this.inputBuffer.slice(0, this.cursorPos) +
|
|
580
|
+
key.char +
|
|
581
|
+
this.inputBuffer.slice(this.cursorPos);
|
|
582
|
+
this.cursorPos += key.char.length;
|
|
583
|
+
this.onNeedsRender?.();
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async sendMessage(userText: string): Promise<void> {
|
|
590
|
+
const serverState = ServerManager.getInstance().getState();
|
|
591
|
+
if (serverState === "warming") {
|
|
592
|
+
this.statusNote = theme.yellow("Aguarde: inicializando proxy...");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const model = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
597
|
+
|
|
598
|
+
this.messages.push({
|
|
599
|
+
role: "user",
|
|
600
|
+
content: userText,
|
|
601
|
+
});
|
|
602
|
+
const assistantMsgIndex = this.messages.length;
|
|
603
|
+
this.messages.push({
|
|
604
|
+
role: "assistant",
|
|
605
|
+
content: "",
|
|
606
|
+
reasoning: "",
|
|
607
|
+
model,
|
|
608
|
+
});
|
|
609
|
+
this.isGenerating = true;
|
|
610
|
+
this.spinnerIndex = 0;
|
|
611
|
+
clearInterval(this.spinnerInterval!);
|
|
612
|
+
this.spinnerInterval = setInterval(() => {
|
|
613
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
|
|
614
|
+
this.scheduleRender();
|
|
615
|
+
}, 100);
|
|
616
|
+
this.statusNote = "";
|
|
617
|
+
this.currentAbortController = new AbortController();
|
|
618
|
+
this.flushRender();
|
|
619
|
+
|
|
620
|
+
const conversationPayload = this.messages
|
|
621
|
+
.slice(0, -1)
|
|
622
|
+
.map((m) => ({ role: m.role, content: m.content }));
|
|
623
|
+
|
|
624
|
+
try {
|
|
625
|
+
const isReasoning = classifyModel(model).category === "Texto & Raciocínio";
|
|
626
|
+
const result = await streamChatCompletions({
|
|
627
|
+
model,
|
|
628
|
+
reasoning_effort: isReasoning ? this.selectedEffort : undefined,
|
|
629
|
+
messages: conversationPayload,
|
|
630
|
+
signal: this.currentAbortController.signal,
|
|
631
|
+
onReasoning: (chunk) => {
|
|
632
|
+
const current = this.messages[assistantMsgIndex];
|
|
633
|
+
if (current) {
|
|
634
|
+
current.reasoning = (current.reasoning || "") + chunk;
|
|
635
|
+
this.scheduleRender();
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
onToken: (chunk) => {
|
|
639
|
+
const current = this.messages[assistantMsgIndex];
|
|
640
|
+
if (current) {
|
|
641
|
+
current.content += chunk;
|
|
642
|
+
this.scheduleRender();
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const current = this.messages[assistantMsgIndex];
|
|
648
|
+
if (current) {
|
|
649
|
+
current.ttfbMs = result.ttfbMs;
|
|
650
|
+
current.totalTimeMs = result.totalTimeMs;
|
|
651
|
+
}
|
|
652
|
+
this.statusNote = theme.green(
|
|
653
|
+
`✓ Resposta concluída em ${(result.totalTimeMs / 1000).toFixed(2)}s (TTFB: ${result.ttfbMs}ms)`,
|
|
654
|
+
);
|
|
655
|
+
} catch (err: any) {
|
|
656
|
+
const current = this.messages[assistantMsgIndex];
|
|
657
|
+
let rawMsg = err?.message || String(err);
|
|
658
|
+
try {
|
|
659
|
+
const jsonMatch = rawMsg.match(/\{.*"error"\s*:\s*\{.*\}\s*\}/s);
|
|
660
|
+
if (jsonMatch) {
|
|
661
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
662
|
+
if (parsed?.error?.message) {
|
|
663
|
+
rawMsg = parsed.error.message;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} catch {}
|
|
667
|
+
|
|
668
|
+
if (
|
|
669
|
+
rawMsg.includes("Target page, context or browser has been closed") ||
|
|
670
|
+
rawMsg.includes("browserType.launchPersistentContext")
|
|
671
|
+
) {
|
|
672
|
+
rawMsg = "Falha ao iniciar o navegador da conta. Feche outras instâncias do QwenProxy ou do Chrome e execute 'qpx reset'.";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (current) {
|
|
676
|
+
current.content = `\n ${theme.red("❌ " + rawMsg)}`;
|
|
677
|
+
}
|
|
678
|
+
this.statusNote = theme.red(`✗ ${rawMsg.slice(0, 80)}`);
|
|
679
|
+
} finally {
|
|
680
|
+
clearInterval(this.spinnerInterval!);
|
|
681
|
+
this.spinnerInterval = null;
|
|
682
|
+
this.isGenerating = false;
|
|
683
|
+
this.currentAbortController = null;
|
|
684
|
+
this.flushRender();
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
|
|
689
|
+
this.lastWidth = width;
|
|
690
|
+
this.lastHeight = height;
|
|
691
|
+
if (snapshot !== undefined) {
|
|
692
|
+
this.lastSnapshot = snapshot ?? null;
|
|
693
|
+
}
|
|
694
|
+
const hasAccounts = !this.lastSnapshot || this.lastSnapshot.accounts.length > 0;
|
|
695
|
+
const totalLines: string[] = [];
|
|
696
|
+
|
|
697
|
+
// 1. Model Header with F2 Shortcut
|
|
698
|
+
const currentModel = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
699
|
+
const currentInfo = classifyModel(currentModel);
|
|
700
|
+
const totalModels = this.availableModels.length;
|
|
701
|
+
const isReasoning = currentInfo.category === "Texto & Raciocínio";
|
|
702
|
+
|
|
703
|
+
const modelLabel = `[ ${currentModel} ]`;
|
|
704
|
+
const styledModel = this.hoveredHeaderBtn === "model"
|
|
705
|
+
? theme.bgHover(` ${theme.bold(theme.white(modelLabel))} `)
|
|
706
|
+
: theme.cyan(modelLabel);
|
|
707
|
+
|
|
708
|
+
const effortLabel = isReasoning
|
|
709
|
+
? this.selectedEffort === "high"
|
|
710
|
+
? "[ Effort: High (Thinking) ]"
|
|
711
|
+
: this.selectedEffort === "medium"
|
|
712
|
+
? "[ Effort: Medium (Auto) ]"
|
|
713
|
+
: "[ Effort: Low (Fast) ]"
|
|
714
|
+
: "";
|
|
715
|
+
|
|
716
|
+
let styledEffort = "";
|
|
717
|
+
if (isReasoning) {
|
|
718
|
+
styledEffort = this.hoveredHeaderBtn === "effort"
|
|
719
|
+
? theme.bgHover(` ${theme.bold(theme.white(effortLabel))} `)
|
|
720
|
+
: this.selectedEffort === "high"
|
|
721
|
+
? theme.green(effortLabel)
|
|
722
|
+
: this.selectedEffort === "medium"
|
|
723
|
+
? theme.yellow(effortLabel)
|
|
724
|
+
: theme.cyan(effortLabel);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const shortcutsLabel = `[ F2: Modelo${isReasoning ? " | F3: Effort" : ""} (${this.selectedModelIndex + 1}/${totalModels}) ]`;
|
|
728
|
+
const styledShortcuts = theme.yellow(shortcutsLabel);
|
|
729
|
+
|
|
730
|
+
// Non-text models show their category badge ([Imagem] / [Vídeo]), while text models omit [Texto]
|
|
731
|
+
const nonTextBadge = !isReasoning && currentInfo.badge ? `${currentInfo.badge} ` : "";
|
|
732
|
+
const nonTextCategory = !isReasoning ? theme.muted(`• ${currentInfo.category}`) : "";
|
|
733
|
+
|
|
734
|
+
const headerLine = hasAccounts
|
|
735
|
+
? ` ${theme.bold("Modelo:")} ${styledModel} ${nonTextBadge}${isReasoning ? styledEffort : nonTextCategory} ${styledShortcuts}`
|
|
736
|
+
: ` ${theme.bold("Modelo:")} ${styledModel} ${theme.yellow("[ [!] Sem Contas: Adicione em [5] Contas ]")}`;
|
|
737
|
+
|
|
738
|
+
// Compute dynamic interactive column bounds:
|
|
739
|
+
const modelStart = 1 + stringWidth(" Modelo: ") + 1; // col 12
|
|
740
|
+
const modelEnd = modelStart + stringWidth(modelLabel) - 1;
|
|
741
|
+
this.modelBtnStartCol = modelStart;
|
|
742
|
+
this.modelBtnEndCol = modelEnd;
|
|
743
|
+
|
|
744
|
+
if (isReasoning) {
|
|
745
|
+
const effortStart = modelEnd + 3; // 2 spaces
|
|
746
|
+
const effortEnd = effortStart + stringWidth(effortLabel) - 1;
|
|
747
|
+
this.effortBtnStartCol = effortStart;
|
|
748
|
+
this.effortBtnEndCol = effortEnd;
|
|
749
|
+
} else {
|
|
750
|
+
this.effortBtnStartCol = 0;
|
|
751
|
+
this.effortBtnEndCol = 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const headerBox = drawBox({
|
|
755
|
+
title: "Chat Tester",
|
|
756
|
+
width,
|
|
757
|
+
height: 3,
|
|
758
|
+
borderColor: theme.borderActive,
|
|
759
|
+
titleColor: theme.blue,
|
|
760
|
+
content: [headerLine],
|
|
761
|
+
});
|
|
762
|
+
totalLines.push(...headerBox);
|
|
763
|
+
|
|
764
|
+
// 2. Main Middle Area: Either Vertical Model Modal OR Chat Conversation
|
|
765
|
+
const chatHeight = Math.max(6, height - 6);
|
|
766
|
+
const innerChatW = width - 4;
|
|
767
|
+
|
|
768
|
+
if (this.isModelModalOpen) {
|
|
769
|
+
const modalLines: string[] = [""];
|
|
770
|
+
for (let i = 0; i < this.availableModels.length; i++) {
|
|
771
|
+
const m = this.availableModels[i];
|
|
772
|
+
const isSel = i === this.modalSelectedIndex;
|
|
773
|
+
const pointer = isSel ? theme.cyan("▸ ") : " ";
|
|
774
|
+
const info = classifyModel(m);
|
|
775
|
+
const line = `${pointer}${info.badge} ${pad(m, 20)} • ${info.category}`;
|
|
776
|
+
modalLines.push(isSel ? theme.bgSelected(line) : line);
|
|
777
|
+
}
|
|
778
|
+
modalLines.push("");
|
|
779
|
+
|
|
780
|
+
const modalBox = drawBox({
|
|
781
|
+
title: "Selecionar Modelo [ Enter: Escolher • Esc: Fechar ]",
|
|
782
|
+
width,
|
|
783
|
+
height: chatHeight,
|
|
784
|
+
borderColor: theme.borderActive,
|
|
785
|
+
titleColor: theme.cyan,
|
|
786
|
+
content: modalLines,
|
|
787
|
+
});
|
|
788
|
+
totalLines.push(...modalBox);
|
|
789
|
+
} else if (this.isEffortModalOpen) {
|
|
790
|
+
const modalLines: string[] = [
|
|
791
|
+
"",
|
|
792
|
+
` ${theme.bold("Modelo:")} ${theme.cyan(currentModel)} (${currentInfo.category})`,
|
|
793
|
+
` ${theme.dim("Escolha o nível de esforço de raciocínio (reasoning_effort):")}`,
|
|
794
|
+
"",
|
|
795
|
+
];
|
|
796
|
+
for (let i = 0; i < this.availableEfforts.length; i++) {
|
|
797
|
+
const eff = this.availableEfforts[i];
|
|
798
|
+
const isSel = i === this.effortSelectedIndex;
|
|
799
|
+
const isCurrent = eff.id === this.selectedEffort;
|
|
800
|
+
const pointer = isSel ? theme.cyan("▸ ") : " ";
|
|
801
|
+
const radio = isCurrent ? theme.green(glyphs.radioOn) : theme.muted(glyphs.radioOff);
|
|
802
|
+
const line = `${pointer}${radio} ${eff.badge} ${pad(eff.label, 18)} • ${eff.desc}`;
|
|
803
|
+
modalLines.push(isSel ? theme.bgSelected(line) : line);
|
|
804
|
+
}
|
|
805
|
+
modalLines.push("");
|
|
806
|
+
|
|
807
|
+
const modalBox = drawBox({
|
|
808
|
+
title: "Nível de Raciocínio / Effort [ Enter: Confirmar • Esc: Manter ]",
|
|
809
|
+
width,
|
|
810
|
+
height: chatHeight,
|
|
811
|
+
borderColor: theme.borderActive,
|
|
812
|
+
titleColor: theme.yellow,
|
|
813
|
+
content: modalLines,
|
|
814
|
+
});
|
|
815
|
+
totalLines.push(...modalBox);
|
|
816
|
+
} else {
|
|
817
|
+
const chatContent: string[] = [];
|
|
818
|
+
|
|
819
|
+
if (this.lastSnapshot && this.lastSnapshot.accounts.length === 0) {
|
|
820
|
+
chatContent.push("");
|
|
821
|
+
chatContent.push(` ${theme.yellow("[!] Nenhuma conta Qwen configurada no servidor.")}`);
|
|
822
|
+
chatContent.push(` ${theme.muted(" Pressione ")}${theme.cyan("Tab")}${theme.muted(" para ir até ")}${theme.bold(theme.white("[5] Contas"))}${theme.muted(" e pressione ")}${theme.bold(theme.white("'A'"))}${theme.muted(" para adicionar seu e-mail e senha.")}`);
|
|
823
|
+
} else if (this.messages.length === 0) {
|
|
824
|
+
chatContent.push("");
|
|
825
|
+
const serverState = ServerManager.getInstance().getState();
|
|
826
|
+
if (serverState === "warming") {
|
|
827
|
+
chatContent.push(theme.yellow(" [!] Inicializando proxy..."));
|
|
828
|
+
} else {
|
|
829
|
+
chatContent.push(theme.muted(" Digite sua mensagem..."));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const msg of this.messages) {
|
|
833
|
+
chatContent.push("");
|
|
834
|
+
if (msg.role === "user") {
|
|
835
|
+
const userLines = msg.content.split(/\r?\n/);
|
|
836
|
+
for (let u = 0; u < userLines.length; u++) {
|
|
837
|
+
if (u === 0) {
|
|
838
|
+
chatContent.push(` ${theme.blue(glyphs.pointer + " Você:")} ${theme.white(userLines[u])}`);
|
|
839
|
+
} else {
|
|
840
|
+
chatContent.push(` ${theme.white(userLines[u])}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
} else {
|
|
844
|
+
const messageModel = msg.model || currentModel;
|
|
845
|
+
chatContent.push(` ${theme.green(glyphs.bullet + " Qwen (" + messageModel + "):")}`);
|
|
846
|
+
// 1. Dedicated Thinking (Reasoning) Container - Opaque, Dimmed, and Cached
|
|
847
|
+
if (msg.reasoning && msg.reasoning.trim().length > 0) {
|
|
848
|
+
const thinkWidth = Math.max(20, innerChatW - 4);
|
|
849
|
+
let thinkLines: string[];
|
|
850
|
+
|
|
851
|
+
if (msg.cachedWidth === innerChatW && msg.cachedReasoningBox) {
|
|
852
|
+
thinkLines = msg.cachedReasoningBox;
|
|
853
|
+
} else {
|
|
854
|
+
const rLines = formatReasoning(msg.reasoning, thinkWidth - 4).map((l) => ` ${l}`);
|
|
855
|
+
if (this.isGenerating && !msg.content && this.messages.indexOf(msg) === this.messages.length - 1) {
|
|
856
|
+
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
857
|
+
rLines.push("");
|
|
858
|
+
rLines.push(` ${theme.yellow(`${spinner} Raciocinando...`)}`);
|
|
859
|
+
}
|
|
860
|
+
thinkLines = drawBox({
|
|
861
|
+
title: "🧠 Raciocínio",
|
|
862
|
+
width: thinkWidth,
|
|
863
|
+
borderColor: theme.borderInactive,
|
|
864
|
+
titleColor: theme.muted,
|
|
865
|
+
content: rLines,
|
|
866
|
+
});
|
|
867
|
+
if (!this.isGenerating) {
|
|
868
|
+
msg.cachedReasoningBox = thinkLines;
|
|
869
|
+
msg.cachedWidth = innerChatW;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
for (const line of thinkLines) {
|
|
874
|
+
chatContent.push(` ${line}`);
|
|
875
|
+
}
|
|
876
|
+
chatContent.push("");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// 2. Final Response - Bright, crisp, full rich Markdown!
|
|
880
|
+
if (msg.content) {
|
|
881
|
+
let contentLines: string[];
|
|
882
|
+
if (msg.cachedWidth === innerChatW && msg.cachedContentLines) {
|
|
883
|
+
contentLines = msg.cachedContentLines;
|
|
884
|
+
} else {
|
|
885
|
+
contentLines = formatMarkdown(msg.content, innerChatW - 6, { dim: false });
|
|
886
|
+
if (!this.isGenerating) {
|
|
887
|
+
msg.cachedContentLines = contentLines;
|
|
888
|
+
msg.cachedWidth = innerChatW;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
for (const line of contentLines) {
|
|
892
|
+
chatContent.push(` ${line}`);
|
|
893
|
+
}
|
|
894
|
+
} else if (this.isGenerating && !msg.reasoning && this.messages.indexOf(msg) === this.messages.length - 1) {
|
|
895
|
+
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
896
|
+
chatContent.push(` ${theme.yellow(`${spinner} Pensando...`)}`);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if (msg.totalTimeMs) {
|
|
900
|
+
chatContent.push(
|
|
901
|
+
` ${theme.dim(`[TTFB: ${msg.ttfbMs}ms | Total: ${(msg.totalTimeMs / 1000).toFixed(2)}s]`)}`,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Flatten and pre-wrap chatContent to innerChatW so every element maps strictly 1:1 to terminal rows
|
|
908
|
+
const flatChatContent: string[] = [];
|
|
909
|
+
for (const item of chatContent) {
|
|
910
|
+
const sub = String(item ?? "").split(/\r?\n/);
|
|
911
|
+
for (const s of sub) {
|
|
912
|
+
if (stringWidth(s) <= innerChatW) {
|
|
913
|
+
flatChatContent.push(s);
|
|
914
|
+
} else {
|
|
915
|
+
flatChatContent.push(...wrapContentLine(s, innerChatW));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Auto-scroll window calculation supporting 1:1 smooth and precise history scrolling
|
|
921
|
+
const visibleCapacity = Math.max(1, chatHeight - 2);
|
|
922
|
+
const total = flatChatContent.length;
|
|
923
|
+
const maxOffset = Math.max(0, total - visibleCapacity);
|
|
924
|
+
this.lastMaxOffset = maxOffset;
|
|
925
|
+
this.lastVisibleCapacity = visibleCapacity;
|
|
926
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
|
|
927
|
+
const clampedOffset = this.scrollOffset;
|
|
928
|
+
const scrollFromTop = maxOffset - clampedOffset;
|
|
929
|
+
|
|
930
|
+
const startIndex = Math.max(0, total - visibleCapacity - clampedOffset);
|
|
931
|
+
const visibleChatLines = flatChatContent.slice(startIndex, startIndex + visibleCapacity);
|
|
932
|
+
|
|
933
|
+
// Lateral scrollbar calculation
|
|
934
|
+
const hasScrollbar = total > visibleCapacity;
|
|
935
|
+
const thumbSize = hasScrollbar
|
|
936
|
+
? Math.max(1, Math.round((visibleCapacity / total) * visibleCapacity))
|
|
937
|
+
: 0;
|
|
938
|
+
const trackRange = Math.max(1, visibleCapacity - thumbSize);
|
|
939
|
+
const thumbTop = hasScrollbar
|
|
940
|
+
? Math.min(
|
|
941
|
+
visibleCapacity - thumbSize,
|
|
942
|
+
Math.max(0, Math.round((scrollFromTop / maxOffset) * trackRange)),
|
|
943
|
+
)
|
|
944
|
+
: 0;
|
|
945
|
+
|
|
946
|
+
const boxInnerW = Math.max(1, width - 2);
|
|
947
|
+
const formattedChatRows: string[] = [];
|
|
948
|
+
for (let r = 0; r < visibleCapacity; r++) {
|
|
949
|
+
if (r < visibleChatLines.length) {
|
|
950
|
+
const line = visibleChatLines[r];
|
|
951
|
+
if (hasScrollbar) {
|
|
952
|
+
const isThumb = r >= thumbTop && r < thumbTop + thumbSize;
|
|
953
|
+
const isHighlighted = this.isScrollbarHovered || this.isDraggingScrollbar;
|
|
954
|
+
let scrollChar: string;
|
|
955
|
+
if (isThumb) {
|
|
956
|
+
scrollChar = isHighlighted ? `\x1b[48;2;45;35;85m\x1b[38;2;0;255;255m\x1b[1m█\x1b[0m` : theme.cyan("█");
|
|
957
|
+
} else {
|
|
958
|
+
scrollChar = isHighlighted ? theme.cyan("│") : theme.dark("│");
|
|
959
|
+
}
|
|
960
|
+
const padded = pad(line, boxInnerW - 1);
|
|
961
|
+
formattedChatRows.push(`${padded}${scrollChar}`);
|
|
962
|
+
} else {
|
|
963
|
+
formattedChatRows.push(line);
|
|
964
|
+
}
|
|
965
|
+
} else {
|
|
966
|
+
formattedChatRows.push("");
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const historyBox = drawBox({
|
|
971
|
+
title: `Conversa (${this.messages.length})`,
|
|
972
|
+
width,
|
|
973
|
+
height: chatHeight,
|
|
974
|
+
borderColor: theme.borderInactive,
|
|
975
|
+
titleColor: theme.lavender,
|
|
976
|
+
content: formattedChatRows,
|
|
977
|
+
});
|
|
978
|
+
totalLines.push(...historyBox);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// 3. Bottom Input Box
|
|
982
|
+
const inputPrompt = " ❯ ";
|
|
983
|
+
const beforeCursor = this.inputBuffer.slice(0, this.cursorPos);
|
|
984
|
+
const atCursor = this.inputBuffer[this.cursorPos] || " ";
|
|
985
|
+
const afterCursor = this.inputBuffer.slice(this.cursorPos + 1);
|
|
986
|
+
const styledCursor = this.isGenerating ? "" : theme.inverse(atCursor);
|
|
987
|
+
const formattedInput = `${beforeCursor}${styledCursor}${afterCursor}`;
|
|
988
|
+
const availableInputW = width - stringWidth(inputPrompt) - 4;
|
|
989
|
+
const displayInput = truncate(formattedInput, availableInputW);
|
|
990
|
+
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
991
|
+
|
|
992
|
+
const inputContent = [`${theme.cyan(inputPrompt)}${displayInput}`];
|
|
993
|
+
const actionLabel = !hasAccounts
|
|
994
|
+
? "[!] Nenhuma conta configurada — adicione uma conta na aba [5] Contas"
|
|
995
|
+
: currentInfo.category === "Geração de Imagem"
|
|
996
|
+
? "Prompt da Imagem"
|
|
997
|
+
: currentInfo.category === "Geração de Vídeo"
|
|
998
|
+
? "Prompt do Vídeo"
|
|
999
|
+
: "Mensagem";
|
|
1000
|
+
|
|
1001
|
+
const inputTitle = this.isGenerating
|
|
1002
|
+
? `${spinner} Gerando... (Esc para cancelar)`
|
|
1003
|
+
: actionLabel;
|
|
1004
|
+
|
|
1005
|
+
const inputBox = drawBox({
|
|
1006
|
+
title: inputTitle,
|
|
1007
|
+
width,
|
|
1008
|
+
height: 3,
|
|
1009
|
+
borderColor: this.isGenerating ? theme.yellow : theme.borderActive,
|
|
1010
|
+
titleColor: this.isGenerating ? theme.yellow : theme.cyan,
|
|
1011
|
+
footer: this.statusNote ? stripAnsi(this.statusNote) : undefined,
|
|
1012
|
+
content: inputContent,
|
|
1013
|
+
});
|
|
1014
|
+
totalLines.push(...inputBox);
|
|
1015
|
+
|
|
1016
|
+
return totalLines;
|
|
1017
|
+
}
|
|
1018
|
+
}
|