blyuecode 0.7.1 → 0.8.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/dist/core/deps.js +99 -0
- package/dist/core/image.js +69 -0
- package/dist/core/media.js +210 -0
- package/dist/core/player.js +208 -0
- package/dist/core/videoPlayer.js +44 -0
- package/dist/core/ytdlp.js +153 -0
- package/dist/index.js +36 -5
- package/dist/ui/Banner.js +10 -3
- package/dist/ui/Chat.js +438 -1
- package/dist/ui/InputBar.js +8 -2
- package/dist/ui/MascotPrompt.js +27 -6
- package/dist/ui/SplashBanner.js +119 -0
- package/dist/ui/splash.js +121 -0
- package/package.json +1 -1
package/dist/ui/Chat.js
CHANGED
|
@@ -26,8 +26,20 @@ import { AuditPanel } from './AuditPanel.js';
|
|
|
26
26
|
import { appendMemory } from '../core/memory.js';
|
|
27
27
|
import { audit } from '../core/audit.js';
|
|
28
28
|
import { listSkills, findSkill, matchSkills, ensureSkillsDir, } from '../core/skills.js';
|
|
29
|
+
import { generateImage } from '../core/image.js';
|
|
30
|
+
import { checkMediaDeps, installMediaDeps } from '../core/deps.js';
|
|
31
|
+
import { searchYouTube, downloadAudio, downloadVideo, parseQuality, estimateSizeMb, } from '../core/media.js';
|
|
32
|
+
import { player } from '../core/player.js';
|
|
33
|
+
import { playVideo } from '../core/videoPlayer.js';
|
|
34
|
+
import { updateYtdlp, ytdlpVersion, ytdlpLogPath, } from '../core/ytdlp.js';
|
|
29
35
|
let nextId = 0;
|
|
30
36
|
const mkItem = (kind, text) => ({ kind, text, id: nextId++ });
|
|
37
|
+
const fmtTime = (s) => {
|
|
38
|
+
const h = Math.floor(s / 3600);
|
|
39
|
+
const m = Math.floor((s % 3600) / 60);
|
|
40
|
+
const sec = Math.floor(s % 60);
|
|
41
|
+
return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0') : m + ':' + String(sec).padStart(2, '0');
|
|
42
|
+
};
|
|
31
43
|
const HistoryLine = ({ item }) => {
|
|
32
44
|
if (item.kind === 'user') {
|
|
33
45
|
return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: pastelAt(0.08), bold: true, children: '❯ ' }), _jsx(Text, { color: theme.text, bold: true, children: item.text })] }));
|
|
@@ -75,6 +87,13 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
75
87
|
const [streaming, setStreaming] = useState('');
|
|
76
88
|
const [overlay, setOverlay] = useState('none');
|
|
77
89
|
const [pendingSkill, setPendingSkill] = useState(null);
|
|
90
|
+
const [pendingMedia, setPendingMedia] = useState(null);
|
|
91
|
+
const [pendingInstall, setPendingInstall] = useState(null);
|
|
92
|
+
const [mediaStatus, setMediaStatus] = useState(null);
|
|
93
|
+
const [playerActive, setPlayerActive] = useState(false);
|
|
94
|
+
const [playProgress, setPlayProgress] = useState(null);
|
|
95
|
+
const [pendingVideoCleanup, setPendingVideoCleanup] = useState(null);
|
|
96
|
+
const [pendingVideoQuality, setPendingVideoQuality] = useState(null);
|
|
78
97
|
const [permReq, setPermReq] = useState(null);
|
|
79
98
|
const [currentModel, setCurrentModel] = useState(process.env.MODEL || '');
|
|
80
99
|
const [agent, setAgent] = useState(() => new Agent());
|
|
@@ -106,6 +125,73 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
106
125
|
const slowColor = useSlowColor();
|
|
107
126
|
useInput((inputChar, key) => {
|
|
108
127
|
bumpActivity();
|
|
128
|
+
if (pendingVideoQuality) {
|
|
129
|
+
const pt = pendingVideoQuality.track;
|
|
130
|
+
const pick = (q) => {
|
|
131
|
+
setPendingVideoQuality(null);
|
|
132
|
+
setHistory((h) => [...h, mkItem('tool', 'quality: ' + q + ' · download and play? [y] yes · [n] cancel')]);
|
|
133
|
+
setPendingMedia({ kind: 'video', track: pt, quality: q });
|
|
134
|
+
};
|
|
135
|
+
if (inputChar === '1') {
|
|
136
|
+
pick('360p');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (inputChar === '2') {
|
|
140
|
+
pick('480p');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (inputChar === '3') {
|
|
144
|
+
pick('720p');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (inputChar === '4') {
|
|
148
|
+
pick('1080p');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (inputChar === '5') {
|
|
152
|
+
pick('best');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
156
|
+
setPendingVideoQuality(null);
|
|
157
|
+
setInput('');
|
|
158
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (pendingMedia) {
|
|
164
|
+
if (inputChar === 'y' || inputChar === 'Y') {
|
|
165
|
+
const pm = pendingMedia;
|
|
166
|
+
setPendingMedia(null);
|
|
167
|
+
setInput('');
|
|
168
|
+
void runPendingMedia(pm);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
172
|
+
setPendingMedia(null);
|
|
173
|
+
setInput('');
|
|
174
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (pendingInstall) {
|
|
180
|
+
if (inputChar === 'y' || inputChar === 'Y') {
|
|
181
|
+
const cmd = pendingInstall;
|
|
182
|
+
setPendingInstall(null);
|
|
183
|
+
setInput('');
|
|
184
|
+
void runPendingInstall(cmd);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (inputChar === 'n' || inputChar === 'N' || key.escape) {
|
|
188
|
+
setPendingInstall(null);
|
|
189
|
+
setInput('');
|
|
190
|
+
setHistory((h) => [...h, mkItem('tool', 'cancelled')]);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
109
195
|
if (permReq)
|
|
110
196
|
return;
|
|
111
197
|
if (key.ctrl && inputChar === 'l')
|
|
@@ -126,6 +212,86 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
126
212
|
return;
|
|
127
213
|
setHistory((h) => [...h, mkItem(kind, text)]);
|
|
128
214
|
};
|
|
215
|
+
const runPendingMedia = async (pm) => {
|
|
216
|
+
if (pm.kind === 'audio') {
|
|
217
|
+
setMediaStatus('downloading: ' + pm.track.title + '…');
|
|
218
|
+
try {
|
|
219
|
+
const d = await downloadAudio(pm.track);
|
|
220
|
+
setMediaStatus('loading into player…');
|
|
221
|
+
await player.play(d);
|
|
222
|
+
setMediaStatus(null);
|
|
223
|
+
setHistory((h) => [...h, mkItem('tool', '▶ ' + d.title + ' (deletes after — /loop /keep)')]);
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
setMediaStatus(null);
|
|
227
|
+
const msg = e?.message || String(e);
|
|
228
|
+
if (/403|forbidden|stale/i.test(msg)) {
|
|
229
|
+
setHistory((h) => [
|
|
230
|
+
...h,
|
|
231
|
+
mkItem('error', msg),
|
|
232
|
+
mkItem('tool', 'run /update-ytdlp to fix (stale yt-dlp)'),
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setHistory((h) => [...h, mkItem('error', msg)]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
setMediaStatus('downloading ' + pm.quality + ': ' + pm.track.title + '…');
|
|
242
|
+
try {
|
|
243
|
+
const v = await downloadVideo(pm.track, pm.quality);
|
|
244
|
+
setMediaStatus('opening player…');
|
|
245
|
+
const { cleanup, notice } = await playVideo(v);
|
|
246
|
+
setMediaStatus(null);
|
|
247
|
+
setPendingVideoCleanup(() => cleanup);
|
|
248
|
+
if (notice) {
|
|
249
|
+
setHistory((h) => [...h, mkItem('tool', '▶ ' + v.title), mkItem('tool', notice)]);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
setHistory((h) => [...h, mkItem('tool', 'video ended · file cleaned up')]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
setMediaStatus(null);
|
|
257
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const runPendingInstall = async (cmd) => {
|
|
262
|
+
setHistory((h) => [...h, mkItem('tool', 'running: ' + cmd)]);
|
|
263
|
+
setLoading(true);
|
|
264
|
+
const r = await installMediaDeps();
|
|
265
|
+
setLoading(false);
|
|
266
|
+
if (r.ok)
|
|
267
|
+
setHistory((h) => [...h, mkItem('tool', '✓ media tools installed')]);
|
|
268
|
+
else
|
|
269
|
+
setHistory((h) => [...h, mkItem('error', 'install failed: ' + r.output.slice(-400))]);
|
|
270
|
+
};
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
const sync = () => {
|
|
273
|
+
try {
|
|
274
|
+
const st = player.getState();
|
|
275
|
+
setPlayerActive(st.playing);
|
|
276
|
+
if (st.playing && st.duration > 0 && st.position >= 0) {
|
|
277
|
+
setPlayProgress({ pos: st.position, dur: st.duration });
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
setPlayProgress(null);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
};
|
|
285
|
+
player.on('update', sync);
|
|
286
|
+
player.on('queue-empty', sync);
|
|
287
|
+
player.on('exit', sync);
|
|
288
|
+
sync();
|
|
289
|
+
return () => {
|
|
290
|
+
player.removeListener('update', sync);
|
|
291
|
+
player.removeListener('queue-empty', sync);
|
|
292
|
+
player.removeListener('exit', sync);
|
|
293
|
+
};
|
|
294
|
+
}, []);
|
|
129
295
|
const handleSubmit = async (text) => {
|
|
130
296
|
bumpActivity();
|
|
131
297
|
if (!text.trim() || loading)
|
|
@@ -219,6 +385,266 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
219
385
|
]);
|
|
220
386
|
return;
|
|
221
387
|
}
|
|
388
|
+
if (trimmed === '/update-ytdlp') {
|
|
389
|
+
setInput('');
|
|
390
|
+
const before = await ytdlpVersion();
|
|
391
|
+
setHistory((h) => [...h, mkItem('tool', 'yt-dlp version: ' + before + ' · updating…')]);
|
|
392
|
+
setLoading(true);
|
|
393
|
+
const r = await updateYtdlp();
|
|
394
|
+
setLoading(false);
|
|
395
|
+
const after = await ytdlpVersion();
|
|
396
|
+
if (r.ok) {
|
|
397
|
+
setHistory((h) => [...h, mkItem('tool', '✓ yt-dlp ' + before + ' → ' + after)]);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
setHistory((h) => [...h, mkItem('error', 'update failed: ' + r.output.slice(-300))]);
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (trimmed === '/ytdlp-log') {
|
|
405
|
+
setInput('');
|
|
406
|
+
const { readYtdlpLog } = await import('../core/ytdlp.js');
|
|
407
|
+
const lines = readYtdlpLog(40);
|
|
408
|
+
if (lines.length === 0) {
|
|
409
|
+
setHistory((h) => [...h, mkItem('tool', 'no yt-dlp errors logged')]);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
setHistory((h) => [
|
|
413
|
+
...h,
|
|
414
|
+
mkItem('tool', 'yt-dlp log: ' + ytdlpLogPath()),
|
|
415
|
+
...lines.map((l) => mkItem('tool', ' ' + l.slice(0, 180))),
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (trimmed === '/install-media') {
|
|
421
|
+
setInput('');
|
|
422
|
+
const deps = await checkMediaDeps();
|
|
423
|
+
if (deps.allPresent) {
|
|
424
|
+
setHistory((h) => [...h, mkItem('tool', 'media tools already installed')]);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const plat = deps.platform;
|
|
428
|
+
if (!plat.installCmd) {
|
|
429
|
+
setHistory((h) => [...h, mkItem('error', 'unknown platform — install yt-dlp ffmpeg mpv manually')]);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
setHistory((h) => [
|
|
433
|
+
...h,
|
|
434
|
+
mkItem('tool', 'will run: ' + plat.installCmd),
|
|
435
|
+
mkItem('tool', '~' + plat.approximateSizeMb + ' MB via ' + plat.updateHint),
|
|
436
|
+
mkItem('tool', 'proceed? [y] yes · [n] cancel'),
|
|
437
|
+
]);
|
|
438
|
+
setPendingInstall(plat.installCmd);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (trimmed.startsWith('/image ')) {
|
|
442
|
+
setInput('');
|
|
443
|
+
const prompt = trimmed.slice(7).trim();
|
|
444
|
+
if (!prompt) {
|
|
445
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /image <prompt>')]);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
setLoading(true);
|
|
449
|
+
setHistory((h) => [...h, mkItem('tool', 'generating image: ' + prompt + '…')]);
|
|
450
|
+
try {
|
|
451
|
+
const r = await generateImage(prompt);
|
|
452
|
+
setHistory((h) => [...h, mkItem('tool', 'saved: ' + r.path), mkItem('tool', 'source: ' + r.url)]);
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
456
|
+
}
|
|
457
|
+
setLoading(false);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (trimmed.startsWith('/play ')) {
|
|
461
|
+
setInput('');
|
|
462
|
+
const q = trimmed.slice(6).trim();
|
|
463
|
+
if (!q) {
|
|
464
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /play <query>')]);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
setMediaStatus('searching: ' + q + '…');
|
|
468
|
+
try {
|
|
469
|
+
const deps = await checkMediaDeps();
|
|
470
|
+
if (!deps.allPresent) {
|
|
471
|
+
setMediaStatus(null);
|
|
472
|
+
setHistory((h) => [...h,
|
|
473
|
+
mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
|
|
474
|
+
mkItem('tool', 'run /install-media to install them'),
|
|
475
|
+
]);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const track = await searchYouTube(q);
|
|
479
|
+
setHistory((h) => [
|
|
480
|
+
...h,
|
|
481
|
+
mkItem('tool', 'found: ' + track.title + ' · ' + fmtTime(track.duration) + (track.uploader ? ' · ' + track.uploader : '')),
|
|
482
|
+
mkItem('tool', 'download and play? [y] yes · [n] cancel'),
|
|
483
|
+
]);
|
|
484
|
+
setMediaStatus(null);
|
|
485
|
+
setPendingMedia({ kind: 'audio', track });
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
setMediaStatus(null);
|
|
489
|
+
const msg = e?.message || String(e);
|
|
490
|
+
if (/403|forbidden|stale/i.test(msg)) {
|
|
491
|
+
setHistory((h) => [
|
|
492
|
+
...h,
|
|
493
|
+
mkItem('error', msg),
|
|
494
|
+
mkItem('tool', 'try /update-ytdlp to fix'),
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
setHistory((h) => [...h, mkItem('error', msg)]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (trimmed.startsWith('/video ')) {
|
|
504
|
+
setInput('');
|
|
505
|
+
const rest = trimmed.slice(7).trim();
|
|
506
|
+
if (!rest) {
|
|
507
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /video <query> [quality]')]);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const parts = rest.split(/\s+/);
|
|
511
|
+
const last = parts[parts.length - 1];
|
|
512
|
+
const qualityPattern = /^(360p?|480p?|720p?|1080p?|best)$/i;
|
|
513
|
+
const qualityPattern2 = /^\d+p?$/i.test(last);
|
|
514
|
+
const looksLikeQuality = qualityPattern.test(last) || qualityPattern2;
|
|
515
|
+
const hasExplicit = qualityPattern.test(last) && parts.length > 1;
|
|
516
|
+
const quality = hasExplicit ? parseQuality(last) : '720p';
|
|
517
|
+
const query = hasExplicit ? parts.slice(0, -1).join(' ') : rest;
|
|
518
|
+
setMediaStatus('searching: ' + query + '…');
|
|
519
|
+
try {
|
|
520
|
+
const deps = await checkMediaDeps();
|
|
521
|
+
if (!deps.allPresent) {
|
|
522
|
+
setMediaStatus(null);
|
|
523
|
+
setHistory((h) => [...h,
|
|
524
|
+
mkItem('tool', 'missing media tools: ' + deps.missing.join(', ')),
|
|
525
|
+
mkItem('tool', 'run /install-media to install them (approval required)'),
|
|
526
|
+
]);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const t = await searchYouTube(query);
|
|
530
|
+
const dur = t.duration;
|
|
531
|
+
const meta = fmtTime(dur) + (t.uploader ? ' · ' + t.uploader : '');
|
|
532
|
+
setMediaStatus(null);
|
|
533
|
+
if (hasExplicit) {
|
|
534
|
+
setHistory((h) => [
|
|
535
|
+
...h,
|
|
536
|
+
mkItem('tool', 'found: ' + t.title + ' · ' + meta),
|
|
537
|
+
mkItem('tool', 'download ' + quality + ' and play? [y] yes · [n] cancel'),
|
|
538
|
+
]);
|
|
539
|
+
setPendingMedia({ kind: 'video', track: t, quality });
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
setHistory((h) => [
|
|
543
|
+
...h,
|
|
544
|
+
mkItem('tool', 'found: ' + t.title + ' · ' + meta),
|
|
545
|
+
mkItem('tool', 'choose quality:'),
|
|
546
|
+
mkItem('tool', ' [1] 360p ~' + estimateSizeMb(dur, '360p') + ' MB'),
|
|
547
|
+
mkItem('tool', ' [2] 480p ~' + estimateSizeMb(dur, '480p') + ' MB'),
|
|
548
|
+
mkItem('tool', ' [3] 720p ~' + estimateSizeMb(dur, '720p') + ' MB'),
|
|
549
|
+
mkItem('tool', ' [4] 1080p ~' + estimateSizeMb(dur, '1080p') + ' MB'),
|
|
550
|
+
mkItem('tool', ' [5] best ~' + estimateSizeMb(dur, 'best') + ' MB'),
|
|
551
|
+
mkItem('tool', 'press 1-5 · [n] cancel'),
|
|
552
|
+
]);
|
|
553
|
+
setPendingVideoQuality({ track: t });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
setMediaStatus(null);
|
|
558
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (trimmed === '/cleanup') {
|
|
563
|
+
setInput('');
|
|
564
|
+
if (pendingVideoCleanup) {
|
|
565
|
+
pendingVideoCleanup();
|
|
566
|
+
setPendingVideoCleanup(null);
|
|
567
|
+
setHistory((h) => [...h, mkItem('tool', 'video file cleaned up')]);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing to clean up')]);
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (trimmed === '/pause') {
|
|
575
|
+
setInput('');
|
|
576
|
+
await player.togglePause();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (trimmed === '/next') {
|
|
580
|
+
setInput('');
|
|
581
|
+
await player.next();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (trimmed === '/stop') {
|
|
585
|
+
setInput('');
|
|
586
|
+
await player.stop();
|
|
587
|
+
setHistory((h) => [...h, mkItem('tool', 'stopped')]);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (trimmed === '/loop') {
|
|
591
|
+
setInput('');
|
|
592
|
+
const on = !player.isLoop();
|
|
593
|
+
player.setLoop(on);
|
|
594
|
+
setHistory((h) => [...h, mkItem('tool', on ? 'loop on' : 'loop off')]);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (trimmed === '/keep') {
|
|
598
|
+
setInput('');
|
|
599
|
+
const st = player.getState();
|
|
600
|
+
if (st.current) {
|
|
601
|
+
st.current.keep = true;
|
|
602
|
+
setHistory((h) => [...h, mkItem('tool', 'will keep: ' + st.current.title)]);
|
|
603
|
+
}
|
|
604
|
+
else
|
|
605
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (trimmed === '/queue') {
|
|
609
|
+
setInput('');
|
|
610
|
+
const st = player.getState();
|
|
611
|
+
if (!st.current && st.queue.length === 0) {
|
|
612
|
+
setHistory((h) => [...h, mkItem('tool', 'nothing playing')]);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const lines = [];
|
|
616
|
+
if (st.current)
|
|
617
|
+
lines.push('▶ ' + st.current.title);
|
|
618
|
+
for (const t of st.queue)
|
|
619
|
+
lines.push(' ⏸ ' + t.title);
|
|
620
|
+
setHistory((h) => [...h, ...lines.map((l) => mkItem('tool', l))]);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (trimmed.startsWith('/remember ')) {
|
|
625
|
+
setInput('');
|
|
626
|
+
const text = trimmed.slice(10).trim();
|
|
627
|
+
if (!text) {
|
|
628
|
+
setHistory((h) => [...h, mkItem('error', 'usage: /remember <text>')]);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
let section = 'Notes';
|
|
632
|
+
let note = text;
|
|
633
|
+
const pipe = text.indexOf('|');
|
|
634
|
+
if (pipe > 0) {
|
|
635
|
+
note = text.slice(0, pipe).trim();
|
|
636
|
+
section = text.slice(pipe + 1).trim() || 'Notes';
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
appendMemory(note, section);
|
|
640
|
+
audit('memory_write', { section, note });
|
|
641
|
+
setHistory((h) => [...h, mkItem('tool', 'remembered (' + section + '): ' + note)]);
|
|
642
|
+
}
|
|
643
|
+
catch (e) {
|
|
644
|
+
setHistory((h) => [...h, mkItem('error', e?.message || String(e))]);
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
222
648
|
if (trimmed === '/debug') {
|
|
223
649
|
setInput('');
|
|
224
650
|
const lines = readDebug();
|
|
@@ -273,6 +699,17 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
273
699
|
mkItem('tool', ' /skill <n> force-activate a skill for next msg'),
|
|
274
700
|
mkItem('tool', ' /compact summarize history to free context'),
|
|
275
701
|
mkItem('tool', ' /debug show last request to provider'),
|
|
702
|
+
mkItem('tool', ' /image <p> generate an image'),
|
|
703
|
+
mkItem('tool', ' /play <q> search + play audio (auto-delete)'),
|
|
704
|
+
mkItem('tool', ' /video <q> [quality] download + play video'),
|
|
705
|
+
mkItem('tool', ' /pause /next /stop /queue playback control'),
|
|
706
|
+
mkItem('tool', ' /loop /keep playback behavior'),
|
|
707
|
+
mkItem('tool', ' /autoplay keep music going when queue ends'),
|
|
708
|
+
mkItem('tool', ' /cleanup delete the video file (Termux, after player closes)'),
|
|
709
|
+
mkItem('tool', ' /install-media install yt-dlp + ffmpeg + mpv'),
|
|
710
|
+
mkItem('tool', ' /update-ytdlp update yt-dlp (fixes HTTP 403)'),
|
|
711
|
+
mkItem('tool', ' /ytdlp-log show last yt-dlp errors'),
|
|
712
|
+
mkItem('tool', ' /remember <text> [| section] append to memory'),
|
|
276
713
|
mkItem('tool', ' /clear clear session and permissions'),
|
|
277
714
|
mkItem('tool', ' /help show this list'),
|
|
278
715
|
]);
|
|
@@ -391,5 +828,5 @@ export const Chat = ({ resumedCount = 0 }) => {
|
|
|
391
828
|
const shortModel = currentModel.length > maxModelLen
|
|
392
829
|
? '…' + currentModel.slice(-(maxModelLen - 1))
|
|
393
830
|
: currentModel || 'unset';
|
|
394
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: streaming }) })) : loading ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading, mascotState: mascotState, elapsedSec: elapsedSec }), _jsxs(Text, { children: [_jsx(Text, { color: slowColor, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/compact" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
831
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: history, children: (item) => _jsx(HistoryLine, { item: item }, item.id) }), mediaStatus ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.accent, children: ["\u266A ", mediaStatus] }) })) : streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text, children: streaming }) })) : loading ? (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, {}) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(InputBar, { value: input, onChange: setInput, onSubmit: handleSubmit, loading: loading, mascotState: playerActive ? 'playing' : mascotState, progress: playProgress, elapsedSec: elapsedSec }), _jsxs(Text, { children: [_jsx(Text, { color: slowColor, children: " /model" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/settings" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: slowColor, children: "/compact" }), _jsx(Text, { color: theme.muted, children: " \u00B7 " }), _jsx(Text, { color: theme.text, children: shortModel })] })] })] }));
|
|
395
832
|
};
|
package/dist/ui/InputBar.js
CHANGED
|
@@ -8,7 +8,7 @@ import { MascotPrompt } from './MascotPrompt.js';
|
|
|
8
8
|
import { theme } from './theme.js';
|
|
9
9
|
import { useSlowColor } from './useSlowColor.js';
|
|
10
10
|
const DOTS = ['●○○○', '○●○○', '○○●○', '○○○●'];
|
|
11
|
-
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, }) => {
|
|
11
|
+
export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awaitingPermission, elapsedSec = 0, progress, }) => {
|
|
12
12
|
const { stdout } = useStdout();
|
|
13
13
|
const width = Math.max(24, stdout?.columns ?? 50);
|
|
14
14
|
const pastel = useSlowColor();
|
|
@@ -28,5 +28,11 @@ export const InputBar = ({ value, onChange, onSubmit, loading, mascotState, awai
|
|
|
28
28
|
const label = awaitingPermission ? 'waiting' : loading ? 'thinking' : 'blyue';
|
|
29
29
|
const status = loading ? ' ' + DOTS[dotI] + ' ' : '';
|
|
30
30
|
const botLen = Math.max(0, width - 2 - status.length);
|
|
31
|
-
|
|
31
|
+
const showProgress = !!(progress && progress.dur > 0 && progress.pos >= 0);
|
|
32
|
+
const fillRatio = showProgress
|
|
33
|
+
? Math.min(1, Math.max(0, progress.pos / progress.dur))
|
|
34
|
+
: 0;
|
|
35
|
+
const fillLen = showProgress ? Math.floor(botLen * fillRatio) : 0;
|
|
36
|
+
const emptyLen = showProgress ? Math.max(0, botLen - fillLen) : botLen;
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(MascotPrompt, { state: state }), _jsx(RocketBar, { label: label, loading: loading, width: width }), _jsxs(Box, { children: [_jsx(Text, { color: pastel, bold: true, children: '❯ ' }), hasText ? (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })) : (_jsxs(_Fragment, { children: [_jsx(Placeholder, {}), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] }))] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: '╰' }), showProgress ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: pastel, bold: true, children: '━'.repeat(fillLen) }), _jsx(Text, { color: theme.muted, children: '─'.repeat(emptyLen) })] })) : (_jsx(Text, { color: theme.muted, children: '─'.repeat(botLen) })), loading && _jsx(Text, { color: pastel, children: status }), _jsx(Text, { color: theme.muted, children: "\u256F" })] })] }));
|
|
32
38
|
};
|
package/dist/ui/MascotPrompt.js
CHANGED
|
@@ -22,13 +22,29 @@ const FACES = {
|
|
|
22
22
|
granted: ['◉‿◉', '◉▽◉', '◉‿◉'],
|
|
23
23
|
denied: ['◔︵◔', '◔_◔', '◔︵◔'],
|
|
24
24
|
impatient: ['◕~◕', '⊙_⊙', '◕~◕', '⊙_⊙'],
|
|
25
|
+
playing: [],
|
|
25
26
|
};
|
|
26
27
|
const SPEEDS = {
|
|
27
28
|
idle: 500, typing: 700, thinking: 120,
|
|
28
29
|
success: 300, error: 400, jump: 70,
|
|
29
30
|
read: 220, write: 260, patch: 260, list: 260, command: 220,
|
|
30
|
-
automate: 200, permission: 450, granted: 180, denied: 350,
|
|
31
|
+
automate: 200, permission: 450, granted: 180, denied: 350,
|
|
32
|
+
impatient: 300, playing: 240,
|
|
31
33
|
};
|
|
34
|
+
const MUSIC_FRAMES = [
|
|
35
|
+
{ top: ' ', mid: ' ', face: '◉◡◉' },
|
|
36
|
+
{ top: ' ♪ ', mid: ' ', face: '◉◡◉' },
|
|
37
|
+
{ top: ' ', mid: ' ♪ ', face: '◉‿◉' },
|
|
38
|
+
{ top: ' ', mid: '♫ ', face: '◕‿◕' },
|
|
39
|
+
{ top: ' ♫', mid: ' ', face: '◕ω◕' },
|
|
40
|
+
{ top: ' ', mid: ' ♪', face: '◉‿◉' },
|
|
41
|
+
{ top: '♪ ', mid: ' ♪ ', face: '◉◡◉' },
|
|
42
|
+
{ top: ' ', mid: ' ', face: '◉◡◉' },
|
|
43
|
+
{ top: ' ♫ ', mid: ' ', face: '◕‿◕' },
|
|
44
|
+
{ top: ' ', mid: '♪ ', face: '◉◠◉' },
|
|
45
|
+
{ top: ' ♪', mid: ' ', face: '◉◡◉' },
|
|
46
|
+
{ top: ' ', mid: ' ♫', face: '◕ᴗ◕' },
|
|
47
|
+
];
|
|
32
48
|
const DROWSY_FACE = '–_–';
|
|
33
49
|
const SLEEPING_FACE = '-‿-';
|
|
34
50
|
const TOP_ROWS = 2;
|
|
@@ -61,7 +77,6 @@ function semanticColor(state) {
|
|
|
61
77
|
return theme.warning;
|
|
62
78
|
}
|
|
63
79
|
export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
64
|
-
const frames = FACES[state];
|
|
65
80
|
const [i, setI] = useState(0);
|
|
66
81
|
const thresholds = defaultIdleThresholds();
|
|
67
82
|
const idle = useIdle({
|
|
@@ -69,19 +84,25 @@ export const MascotPrompt = ({ state, keepAwake = false, }) => {
|
|
|
69
84
|
disabled: keepAwake || idleDisabled(),
|
|
70
85
|
});
|
|
71
86
|
const slowColor = useSlowColor();
|
|
87
|
+
const frameCount = state === 'playing' ? MUSIC_FRAMES.length : FACES[state].length;
|
|
72
88
|
useEffect(() => {
|
|
73
89
|
setI(0);
|
|
74
|
-
if (
|
|
90
|
+
if (frameCount <= 1)
|
|
75
91
|
return;
|
|
76
|
-
const t = setInterval(() => setI((v) => (v + 1) %
|
|
92
|
+
const t = setInterval(() => setI((v) => (v + 1) % frameCount), SPEEDS[state]);
|
|
77
93
|
return () => clearInterval(t);
|
|
78
|
-
}, [state,
|
|
94
|
+
}, [state, frameCount]);
|
|
79
95
|
if (idle === 'sleeping') {
|
|
80
96
|
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Zzz, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + SLEEPING_FACE })] }));
|
|
81
97
|
}
|
|
82
98
|
if (idle === 'drowsy') {
|
|
83
99
|
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: theme.muted, bold: true, children: ' ' + DROWSY_FACE })] }));
|
|
84
100
|
}
|
|
101
|
+
if (state === 'playing') {
|
|
102
|
+
const fr = MUSIC_FRAMES[i % MUSIC_FRAMES.length];
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(Text, { color: slowColor, children: fr.top }), _jsx(Text, { color: slowColor, children: fr.mid }), _jsx(Text, { color: slowColor, bold: true, children: ' ' + fr.face })] }));
|
|
104
|
+
}
|
|
105
|
+
const frames = FACES[state];
|
|
85
106
|
const color = isSemantic(state) ? semanticColor(state) : slowColor;
|
|
86
|
-
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i] })] }));
|
|
107
|
+
return (_jsxs(Box, { flexDirection: "column", height: TOP_ROWS + 1, children: [_jsx(BlankRows, {}), _jsx(Text, { color: color, bold: true, children: ' ' + frames[i % frames.length] })] }));
|
|
87
108
|
};
|