agent-relay-server 0.12.4 → 0.14.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/docs/openapi.json +310 -1
- package/package.json +8 -5
- package/public/index.html +944 -12
- package/src/cli.ts +82 -0
- package/src/config-store.ts +71 -0
- package/src/db.ts +15 -0
- package/src/insights-db.ts +179 -0
- package/src/routes.ts +110 -0
package/public/index.html
CHANGED
|
@@ -10286,6 +10286,35 @@ async function apiUpload(path, file, meta) {
|
|
|
10286
10286
|
}
|
|
10287
10287
|
return response.json();
|
|
10288
10288
|
}
|
|
10289
|
+
async function apiPostAudio(path, blob) {
|
|
10290
|
+
const headers = { "Content-Type": blob.type || "application/octet-stream" };
|
|
10291
|
+
if (authToken) headers["X-Agent-Relay-Token"] = authToken;
|
|
10292
|
+
const response = await fetch(new URL("api" + path, baseUrl()), {
|
|
10293
|
+
method: "POST",
|
|
10294
|
+
headers,
|
|
10295
|
+
body: blob
|
|
10296
|
+
});
|
|
10297
|
+
if (!response.ok) {
|
|
10298
|
+
if (response.status === 401) throw makeError(401, "Authentication required");
|
|
10299
|
+
throw makeError(response.status, await responseErrorMessage(response));
|
|
10300
|
+
}
|
|
10301
|
+
return response.json();
|
|
10302
|
+
}
|
|
10303
|
+
/** POST JSON and get an audio (or other) Blob back — used for server-side TTS. */
|
|
10304
|
+
async function apiPostJsonForBlob(path, body) {
|
|
10305
|
+
const headers = { "Content-Type": "application/json" };
|
|
10306
|
+
if (authToken) headers["X-Agent-Relay-Token"] = authToken;
|
|
10307
|
+
const response = await fetch(new URL("api" + path, baseUrl()), {
|
|
10308
|
+
method: "POST",
|
|
10309
|
+
headers,
|
|
10310
|
+
body: JSON.stringify(body)
|
|
10311
|
+
});
|
|
10312
|
+
if (!response.ok) {
|
|
10313
|
+
if (response.status === 401) throw makeError(401, "Authentication required");
|
|
10314
|
+
throw makeError(response.status, await responseErrorMessage(response));
|
|
10315
|
+
}
|
|
10316
|
+
return response.blob();
|
|
10317
|
+
}
|
|
10289
10318
|
async function apiBlob(path) {
|
|
10290
10319
|
const opts = {
|
|
10291
10320
|
method: "GET",
|
|
@@ -10301,6 +10330,290 @@ async function apiBlob(path) {
|
|
|
10301
10330
|
return response.blob();
|
|
10302
10331
|
}
|
|
10303
10332
|
//#endregion
|
|
10333
|
+
//#region src/lib/voice.ts
|
|
10334
|
+
/**
|
|
10335
|
+
* Browser voice I/O for chat.
|
|
10336
|
+
*
|
|
10337
|
+
* TTS (output) is 100% browser-native via the Web Speech API — the store
|
|
10338
|
+
* already receives every agent response turn, so there is no backend round-trip.
|
|
10339
|
+
* STT (input) records the mic and posts the clip to the voice connector's
|
|
10340
|
+
* whisper endpoint through the relay proxy (single-origin, auth-gated).
|
|
10341
|
+
*
|
|
10342
|
+
* Speaker policy ("active chat owns the speaker"):
|
|
10343
|
+
* - One utterance at a time. Disabled => silent.
|
|
10344
|
+
* - Only the active chat speaks, and only responses that arrive while it is
|
|
10345
|
+
* active (no backlog replay).
|
|
10346
|
+
* - Switching away lets the current utterance finish but drops the now-background
|
|
10347
|
+
* chat's queued + future responses.
|
|
10348
|
+
* - The active chat preempts: if it speaks while a previous chat's audio lingers,
|
|
10349
|
+
* that audio is cancelled.
|
|
10350
|
+
*/
|
|
10351
|
+
/** Collapse markdown/code into something worth hearing (mirrors the connector's text.ts). */
|
|
10352
|
+
function speechify(markdown) {
|
|
10353
|
+
if (!markdown) return "";
|
|
10354
|
+
let text = markdown.replace(/\r\n/g, "\n");
|
|
10355
|
+
text = text.replace(/```[^\n]*\n([\s\S]*?)```/g, (_m, body) => {
|
|
10356
|
+
const lines = body.replace(/\n+$/, "").split("\n").length;
|
|
10357
|
+
return ` code block, ${lines} ${lines === 1 ? "line" : "lines"}. `;
|
|
10358
|
+
});
|
|
10359
|
+
text = text.replace(/```[^\n]*/g, " code block. ");
|
|
10360
|
+
text = text.replace(/^#{1,6}\s+/gm, "");
|
|
10361
|
+
text = text.replace(/^\s*[-*+]\s+/gm, ". ");
|
|
10362
|
+
text = text.replace(/^\s*\d+\.\s+/gm, ". ");
|
|
10363
|
+
text = text.replace(/`([^`]+)`/g, "$1");
|
|
10364
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
10365
|
+
text = text.replace(/(\*\*|__|\*|_)(.*?)\1/g, "$2");
|
|
10366
|
+
text = text.replace(/[ \t]+/g, " ");
|
|
10367
|
+
text = text.replace(/\n{2,}/g, ". ");
|
|
10368
|
+
text = text.replace(/\s*\.\s*\.\s*(\.\s*)+/g, ". ");
|
|
10369
|
+
return text.trim();
|
|
10370
|
+
}
|
|
10371
|
+
var MAX_CHUNK = 220;
|
|
10372
|
+
/** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
|
|
10373
|
+
function chunkForSpeech(text) {
|
|
10374
|
+
const sentences = text.match(/[^.!?]+[.!?]*\s*/g)?.map((s) => s.trim()).filter(Boolean) ?? [text];
|
|
10375
|
+
const out = [];
|
|
10376
|
+
for (const s of sentences) {
|
|
10377
|
+
if (s.length <= MAX_CHUNK) {
|
|
10378
|
+
out.push(s);
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
let rest = s;
|
|
10382
|
+
while (rest.length > MAX_CHUNK) {
|
|
10383
|
+
let cut = rest.lastIndexOf(" ", MAX_CHUNK);
|
|
10384
|
+
if (cut <= 0) cut = MAX_CHUNK;
|
|
10385
|
+
out.push(rest.slice(0, cut).trim());
|
|
10386
|
+
rest = rest.slice(cut).trim();
|
|
10387
|
+
}
|
|
10388
|
+
if (rest) out.push(rest);
|
|
10389
|
+
}
|
|
10390
|
+
return out;
|
|
10391
|
+
}
|
|
10392
|
+
var synthAvailable = typeof window !== "undefined" && "speechSynthesis" in window;
|
|
10393
|
+
var VoiceTts = class {
|
|
10394
|
+
enabled = false;
|
|
10395
|
+
lang = "en-US";
|
|
10396
|
+
mode = "kokoro";
|
|
10397
|
+
kokoroVoice = "am_michael";
|
|
10398
|
+
active = null;
|
|
10399
|
+
queue = [];
|
|
10400
|
+
currentChat = null;
|
|
10401
|
+
speaking = false;
|
|
10402
|
+
gen = 0;
|
|
10403
|
+
audioEl = null;
|
|
10404
|
+
audioUrl = null;
|
|
10405
|
+
get available() {
|
|
10406
|
+
return synthAvailable || typeof Audio !== "undefined";
|
|
10407
|
+
}
|
|
10408
|
+
isEnabled() {
|
|
10409
|
+
return this.enabled;
|
|
10410
|
+
}
|
|
10411
|
+
setEnabled(on) {
|
|
10412
|
+
if (on === this.enabled) return;
|
|
10413
|
+
this.enabled = on;
|
|
10414
|
+
if (!on) this.reset();
|
|
10415
|
+
}
|
|
10416
|
+
/** Set the spoken-voice language (BCP-47, e.g. "en-US"). Empty = browser locale. */
|
|
10417
|
+
setLang(lang) {
|
|
10418
|
+
this.lang = lang;
|
|
10419
|
+
}
|
|
10420
|
+
setMode(mode) {
|
|
10421
|
+
if (mode === this.mode) return;
|
|
10422
|
+
this.mode = mode;
|
|
10423
|
+
this.reset();
|
|
10424
|
+
}
|
|
10425
|
+
setKokoroVoice(voice) {
|
|
10426
|
+
this.kokoroVoice = voice;
|
|
10427
|
+
}
|
|
10428
|
+
setActiveChat(chatId) {
|
|
10429
|
+
if (chatId === this.active) return;
|
|
10430
|
+
this.active = chatId;
|
|
10431
|
+
this.queue = this.queue.filter((q) => q.chatId === chatId);
|
|
10432
|
+
}
|
|
10433
|
+
/** A captured agent response turn arrived for `chatId`. */
|
|
10434
|
+
onResponse(chatId, rawText) {
|
|
10435
|
+
if (!this.enabled || !this.available || !chatId || chatId !== this.active) return;
|
|
10436
|
+
const text = speechify(rawText);
|
|
10437
|
+
if (!text) return;
|
|
10438
|
+
if (this.speaking && this.currentChat && this.currentChat !== chatId) {
|
|
10439
|
+
this.queue = [];
|
|
10440
|
+
this.cancel();
|
|
10441
|
+
}
|
|
10442
|
+
this.queue.push({
|
|
10443
|
+
chatId,
|
|
10444
|
+
text
|
|
10445
|
+
});
|
|
10446
|
+
this.pump();
|
|
10447
|
+
}
|
|
10448
|
+
/** Cut all speech immediately (e.g. when the user starts talking). */
|
|
10449
|
+
bargeIn() {
|
|
10450
|
+
this.reset();
|
|
10451
|
+
}
|
|
10452
|
+
reset() {
|
|
10453
|
+
this.queue = [];
|
|
10454
|
+
this.cancel();
|
|
10455
|
+
}
|
|
10456
|
+
cancel() {
|
|
10457
|
+
this.gen++;
|
|
10458
|
+
this.currentChat = null;
|
|
10459
|
+
this.speaking = false;
|
|
10460
|
+
try {
|
|
10461
|
+
window.speechSynthesis.cancel();
|
|
10462
|
+
} catch {}
|
|
10463
|
+
this.stopAudio();
|
|
10464
|
+
}
|
|
10465
|
+
stopAudio() {
|
|
10466
|
+
if (this.audioEl) try {
|
|
10467
|
+
this.audioEl.pause();
|
|
10468
|
+
this.audioEl.src = "";
|
|
10469
|
+
} catch {}
|
|
10470
|
+
if (this.audioUrl) {
|
|
10471
|
+
try {
|
|
10472
|
+
URL.revokeObjectURL(this.audioUrl);
|
|
10473
|
+
} catch {}
|
|
10474
|
+
this.audioUrl = null;
|
|
10475
|
+
}
|
|
10476
|
+
}
|
|
10477
|
+
pump() {
|
|
10478
|
+
if (this.speaking) return;
|
|
10479
|
+
const item = this.queue.shift();
|
|
10480
|
+
if (!item) return;
|
|
10481
|
+
this.speaking = true;
|
|
10482
|
+
this.currentChat = item.chatId;
|
|
10483
|
+
const gen = ++this.gen;
|
|
10484
|
+
const chunks = chunkForSpeech(item.text);
|
|
10485
|
+
const done = () => {
|
|
10486
|
+
if (gen !== this.gen) return;
|
|
10487
|
+
this.speaking = false;
|
|
10488
|
+
this.currentChat = null;
|
|
10489
|
+
this.pump();
|
|
10490
|
+
};
|
|
10491
|
+
if (this.mode === "kokoro") this.speakKokoro(chunks, 0, gen, done);
|
|
10492
|
+
else this.speakBrowser(chunks, 0, gen, done);
|
|
10493
|
+
}
|
|
10494
|
+
speakBrowser(chunks, i, gen, done) {
|
|
10495
|
+
if (gen !== this.gen) return;
|
|
10496
|
+
if (!synthAvailable || i >= chunks.length) return done();
|
|
10497
|
+
const u = new SpeechSynthesisUtterance(chunks[i]);
|
|
10498
|
+
u.lang = this.lang || navigator.language || "en-US";
|
|
10499
|
+
u.onend = () => this.speakBrowser(chunks, i + 1, gen, done);
|
|
10500
|
+
u.onerror = () => this.speakBrowser(chunks, i + 1, gen, done);
|
|
10501
|
+
window.speechSynthesis.speak(u);
|
|
10502
|
+
}
|
|
10503
|
+
speakKokoro(chunks, i, gen, done, prefetched) {
|
|
10504
|
+
if (gen !== this.gen) return;
|
|
10505
|
+
const text = chunks[i];
|
|
10506
|
+
if (text === void 0) return done();
|
|
10507
|
+
const cur = prefetched ?? this.fetchSpeech(text);
|
|
10508
|
+
const nextText = chunks[i + 1];
|
|
10509
|
+
const next = nextText !== void 0 ? this.fetchSpeech(nextText) : void 0;
|
|
10510
|
+
cur.then((blob) => {
|
|
10511
|
+
if (gen !== this.gen) return;
|
|
10512
|
+
this.playBlob(blob, gen, () => this.speakKokoro(chunks, i + 1, gen, done, next), () => {
|
|
10513
|
+
this.speakBrowser(chunks, i, gen, done);
|
|
10514
|
+
});
|
|
10515
|
+
}).catch(() => {
|
|
10516
|
+
if (gen !== this.gen) return;
|
|
10517
|
+
this.speakBrowser(chunks, i, gen, done);
|
|
10518
|
+
});
|
|
10519
|
+
}
|
|
10520
|
+
fetchSpeech(text) {
|
|
10521
|
+
return apiPostJsonForBlob("/connectors/voice/call/speak", {
|
|
10522
|
+
text,
|
|
10523
|
+
voice: this.kokoroVoice
|
|
10524
|
+
});
|
|
10525
|
+
}
|
|
10526
|
+
playBlob(blob, gen, onend, onerror) {
|
|
10527
|
+
if (gen !== this.gen) return;
|
|
10528
|
+
if (typeof Audio === "undefined") return onerror();
|
|
10529
|
+
this.stopAudio();
|
|
10530
|
+
if (!this.audioEl) this.audioEl = new Audio();
|
|
10531
|
+
const url = URL.createObjectURL(blob);
|
|
10532
|
+
this.audioUrl = url;
|
|
10533
|
+
const el = this.audioEl;
|
|
10534
|
+
el.src = url;
|
|
10535
|
+
el.onended = () => {
|
|
10536
|
+
if (gen === this.gen) onend();
|
|
10537
|
+
};
|
|
10538
|
+
el.onerror = () => {
|
|
10539
|
+
if (gen === this.gen) onerror();
|
|
10540
|
+
};
|
|
10541
|
+
el.play().catch(() => {
|
|
10542
|
+
if (gen === this.gen) onerror();
|
|
10543
|
+
});
|
|
10544
|
+
}
|
|
10545
|
+
};
|
|
10546
|
+
var voiceTts = new VoiceTts();
|
|
10547
|
+
/** Sorted unique BCP-47 languages the browser's speech engine can speak. May be empty
|
|
10548
|
+
* until the engine finishes loading voices (listen for `voiceschanged` and re-read). */
|
|
10549
|
+
function availableSpeechLangs() {
|
|
10550
|
+
if (!synthAvailable) return [];
|
|
10551
|
+
try {
|
|
10552
|
+
return [...new Set(window.speechSynthesis.getVoices().map((v) => v.lang).filter(Boolean))].sort();
|
|
10553
|
+
} catch {
|
|
10554
|
+
return [];
|
|
10555
|
+
}
|
|
10556
|
+
}
|
|
10557
|
+
var micAvailable = typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof window !== "undefined" && "MediaRecorder" in window;
|
|
10558
|
+
function pickMimeType() {
|
|
10559
|
+
for (const m of [
|
|
10560
|
+
"audio/webm;codecs=opus",
|
|
10561
|
+
"audio/webm",
|
|
10562
|
+
"audio/ogg;codecs=opus",
|
|
10563
|
+
"audio/mp4"
|
|
10564
|
+
]) if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported?.(m)) return m;
|
|
10565
|
+
}
|
|
10566
|
+
/** Hold-to-talk mic recorder. start() on press, stop() on release returns the clip. */
|
|
10567
|
+
var PttRecorder = class {
|
|
10568
|
+
rec = null;
|
|
10569
|
+
chunks = [];
|
|
10570
|
+
stream = null;
|
|
10571
|
+
async start() {
|
|
10572
|
+
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
10573
|
+
this.chunks = [];
|
|
10574
|
+
const mime = pickMimeType();
|
|
10575
|
+
this.rec = new MediaRecorder(this.stream, mime ? { mimeType: mime } : void 0);
|
|
10576
|
+
this.rec.ondataavailable = (e) => {
|
|
10577
|
+
if (e.data.size) this.chunks.push(e.data);
|
|
10578
|
+
};
|
|
10579
|
+
this.rec.start();
|
|
10580
|
+
}
|
|
10581
|
+
async stop() {
|
|
10582
|
+
const rec = this.rec;
|
|
10583
|
+
if (!rec) {
|
|
10584
|
+
this.teardown();
|
|
10585
|
+
return null;
|
|
10586
|
+
}
|
|
10587
|
+
const stopped = new Promise((resolve) => {
|
|
10588
|
+
rec.onstop = () => resolve();
|
|
10589
|
+
});
|
|
10590
|
+
try {
|
|
10591
|
+
rec.stop();
|
|
10592
|
+
} catch {}
|
|
10593
|
+
await stopped;
|
|
10594
|
+
const type = rec.mimeType || "audio/webm";
|
|
10595
|
+
const blob = new Blob(this.chunks, { type });
|
|
10596
|
+
this.teardown();
|
|
10597
|
+
return blob.size ? blob : null;
|
|
10598
|
+
}
|
|
10599
|
+
cancel() {
|
|
10600
|
+
try {
|
|
10601
|
+
this.rec?.stop();
|
|
10602
|
+
} catch {}
|
|
10603
|
+
this.teardown();
|
|
10604
|
+
}
|
|
10605
|
+
teardown() {
|
|
10606
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
10607
|
+
this.rec = null;
|
|
10608
|
+
this.stream = null;
|
|
10609
|
+
this.chunks = [];
|
|
10610
|
+
}
|
|
10611
|
+
};
|
|
10612
|
+
/** Send a recorded clip through the relay proxy to the voice connector's whisper STT. */
|
|
10613
|
+
async function transcribeClip(blob) {
|
|
10614
|
+
return ((await apiPostAudio("/connectors/voice/call/transcribe", blob))?.text ?? "").trim();
|
|
10615
|
+
}
|
|
10616
|
+
//#endregion
|
|
10304
10617
|
//#region src/store/clock.ts
|
|
10305
10618
|
var useClock = create$1((set, get) => ({
|
|
10306
10619
|
now: Date.now(),
|
|
@@ -10474,6 +10787,11 @@ var NAV_ITEMS = [
|
|
|
10474
10787
|
label: "Analytics",
|
|
10475
10788
|
icon: "AreaChart"
|
|
10476
10789
|
},
|
|
10790
|
+
{
|
|
10791
|
+
key: "insights",
|
|
10792
|
+
label: "Insights",
|
|
10793
|
+
icon: "Lightbulb"
|
|
10794
|
+
},
|
|
10477
10795
|
{
|
|
10478
10796
|
key: "maintenance",
|
|
10479
10797
|
label: "Maintenance",
|
|
@@ -10701,6 +11019,11 @@ function inboxPeer(msg) {
|
|
|
10701
11019
|
function isHumanInboundMessage(msg) {
|
|
10702
11020
|
return msg.to === "user" && msg.from !== "user";
|
|
10703
11021
|
}
|
|
11022
|
+
function isSessionActivityStep(msg) {
|
|
11023
|
+
if (msg.kind !== "session") return false;
|
|
11024
|
+
const type = (msg.payload?.session)?.type;
|
|
11025
|
+
return type === "reasoning" || type === "tool";
|
|
11026
|
+
}
|
|
10704
11027
|
function isClaimableTaskWaiting(task) {
|
|
10705
11028
|
return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
|
|
10706
11029
|
}
|
|
@@ -11636,6 +11959,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11636
11959
|
showOffline: false,
|
|
11637
11960
|
showBuiltIns: false,
|
|
11638
11961
|
autoRefresh: true,
|
|
11962
|
+
voiceTtsEnabled: false,
|
|
11963
|
+
voiceTtsLang: "en-US",
|
|
11964
|
+
voiceTtsMode: "kokoro",
|
|
11965
|
+
voiceTtsKokoroVoice: "am_michael",
|
|
11639
11966
|
agentSort: "status",
|
|
11640
11967
|
agentSortDir: "asc",
|
|
11641
11968
|
agentPresetFilter: "",
|
|
@@ -11823,6 +12150,22 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11823
12150
|
_refreshTimer: null,
|
|
11824
12151
|
_refreshInFlight: false,
|
|
11825
12152
|
set: (partial) => set(partial),
|
|
12153
|
+
setVoiceTtsEnabled(on) {
|
|
12154
|
+
voiceTts.setEnabled(on);
|
|
12155
|
+
set({ voiceTtsEnabled: on });
|
|
12156
|
+
},
|
|
12157
|
+
setVoiceTtsLang(lang) {
|
|
12158
|
+
voiceTts.setLang(lang);
|
|
12159
|
+
set({ voiceTtsLang: lang });
|
|
12160
|
+
},
|
|
12161
|
+
setVoiceTtsMode(mode) {
|
|
12162
|
+
voiceTts.setMode(mode);
|
|
12163
|
+
set({ voiceTtsMode: mode });
|
|
12164
|
+
},
|
|
12165
|
+
setVoiceTtsKokoroVoice(voice) {
|
|
12166
|
+
voiceTts.setKokoroVoice(voice);
|
|
12167
|
+
set({ voiceTtsKokoroVoice: voice });
|
|
12168
|
+
},
|
|
11826
12169
|
async init() {
|
|
11827
12170
|
if (!useRelayStore.persist.hasHydrated()) await new Promise((resolve) => {
|
|
11828
12171
|
const unsub = useRelayStore.persist.onFinishHydration(() => {
|
|
@@ -11832,6 +12175,11 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11832
12175
|
});
|
|
11833
12176
|
const token = get().authToken;
|
|
11834
12177
|
if (token) setAuthToken(token);
|
|
12178
|
+
voiceTts.setEnabled(get().voiceTtsEnabled);
|
|
12179
|
+
voiceTts.setLang(get().voiceTtsLang);
|
|
12180
|
+
voiceTts.setMode(get().voiceTtsMode);
|
|
12181
|
+
voiceTts.setKokoroVoice(get().voiceTtsKokoroVoice);
|
|
12182
|
+
syncVoiceActiveChat(get());
|
|
11835
12183
|
setUnauthorizedHandler(() => {
|
|
11836
12184
|
if (!get().authNeeded) set({
|
|
11837
12185
|
authNeeded: true,
|
|
@@ -12345,6 +12693,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12345
12693
|
const msgs = [...s.messages, msg];
|
|
12346
12694
|
if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
|
|
12347
12695
|
set({ messages: msgs });
|
|
12696
|
+
if (msg.kind === "session" && msg.from !== "user") {
|
|
12697
|
+
const sess = msg.payload?.session;
|
|
12698
|
+
if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
|
|
12699
|
+
}
|
|
12348
12700
|
const peer = inboxPeer(msg);
|
|
12349
12701
|
if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
|
|
12350
12702
|
return;
|
|
@@ -13914,6 +14266,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13914
14266
|
showOffline: state.showOffline,
|
|
13915
14267
|
showBuiltIns: state.showBuiltIns,
|
|
13916
14268
|
autoRefresh: state.autoRefresh,
|
|
14269
|
+
voiceTtsEnabled: state.voiceTtsEnabled,
|
|
14270
|
+
voiceTtsLang: state.voiceTtsLang,
|
|
14271
|
+
voiceTtsMode: state.voiceTtsMode,
|
|
14272
|
+
voiceTtsKokoroVoice: state.voiceTtsKokoroVoice,
|
|
13917
14273
|
agentSort: state.agentSort,
|
|
13918
14274
|
agentSortDir: state.agentSortDir,
|
|
13919
14275
|
agentPresetFilter: state.agentPresetFilter,
|
|
@@ -13953,6 +14309,14 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13953
14309
|
spawnWorkspaceMode: state.spawnWorkspaceMode
|
|
13954
14310
|
})
|
|
13955
14311
|
}));
|
|
14312
|
+
var _voiceActiveChat = null;
|
|
14313
|
+
function syncVoiceActiveChat(s) {
|
|
14314
|
+
const active = s.voiceTtsEnabled && s.view === "chat" ? s.selectedInboxThread || null : null;
|
|
14315
|
+
if (active === _voiceActiveChat) return;
|
|
14316
|
+
_voiceActiveChat = active;
|
|
14317
|
+
voiceTts.setActiveChat(active);
|
|
14318
|
+
}
|
|
14319
|
+
useRelayStore.subscribe(syncVoiceActiveChat);
|
|
13956
14320
|
//#endregion
|
|
13957
14321
|
//#region src/lib/themes.ts
|
|
13958
14322
|
var THEMES = [
|
|
@@ -25055,7 +25419,7 @@ function useAgentAttention(agent) {
|
|
|
25055
25419
|
const peerMessages = messages.filter((m) => inboxPeer(m) === agent.id && !isReactionEventMessage(m));
|
|
25056
25420
|
const cursor = Number(readCursors[agent.id] || 0);
|
|
25057
25421
|
const archivedAt = Number(archivedThreads[agent.id] || 0);
|
|
25058
|
-
const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25422
|
+
const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25059
25423
|
const pendingPairInvite = pairs.find((p) => (p.status === "active" || p.status === "pending") && (p.requesterId === agent.id || p.targetId === agent.id))?.status === "pending";
|
|
25060
25424
|
const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) && targetMatchesAgent(t.target, agent)).length + messages.filter((m) => isClaimableMessageWaiting(m) && targetMatchesAgent(m.to, agent)).length;
|
|
25061
25425
|
return {
|
|
@@ -25154,6 +25518,7 @@ function useAllInboxThreads() {
|
|
|
25154
25518
|
peer,
|
|
25155
25519
|
messages: [],
|
|
25156
25520
|
lastMessage: null,
|
|
25521
|
+
previewMessage: null,
|
|
25157
25522
|
attention: {
|
|
25158
25523
|
unread: 0,
|
|
25159
25524
|
needsHumanResponse: false,
|
|
@@ -25167,8 +25532,17 @@ function useAllInboxThreads() {
|
|
|
25167
25532
|
for (const thread of threads.values()) {
|
|
25168
25533
|
thread.messages.sort((a, b) => a.id - b.id);
|
|
25169
25534
|
thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
|
|
25535
|
+
let preview = null;
|
|
25536
|
+
for (let i = thread.messages.length - 1; i >= 0; i--) {
|
|
25537
|
+
const m = thread.messages[i];
|
|
25538
|
+
if (m && !isSessionActivityStep(m)) {
|
|
25539
|
+
preview = m;
|
|
25540
|
+
break;
|
|
25541
|
+
}
|
|
25542
|
+
}
|
|
25543
|
+
thread.previewMessage = preview || thread.lastMessage;
|
|
25170
25544
|
const cursor = Number(readCursors[thread.peer] || 0);
|
|
25171
|
-
const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25545
|
+
const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
|
|
25172
25546
|
thread.attention = {
|
|
25173
25547
|
unread,
|
|
25174
25548
|
needsHumanResponse: false,
|
|
@@ -76130,7 +76504,8 @@ var iconMap = {
|
|
|
76130
76504
|
FolderTree,
|
|
76131
76505
|
Shield,
|
|
76132
76506
|
Wrench,
|
|
76133
|
-
UserCog
|
|
76507
|
+
UserCog,
|
|
76508
|
+
Lightbulb
|
|
76134
76509
|
};
|
|
76135
76510
|
function notificationTime(notification) {
|
|
76136
76511
|
return new Date(notification.createdAt).toLocaleTimeString(void 0, {
|
|
@@ -125201,7 +125576,7 @@ function AgentListPanel({ threads, onSelectAgent }) {
|
|
|
125201
125576
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
|
|
125202
125577
|
const thread = threadByPeer.get(agent.id);
|
|
125203
125578
|
const unread = thread?.attention.unread || 0;
|
|
125204
|
-
const lastMsg = thread?.
|
|
125579
|
+
const lastMsg = thread?.previewMessage;
|
|
125205
125580
|
const lastActivityAt = threadActivityTimestamp(thread);
|
|
125206
125581
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
125207
125582
|
className: cn$2("w-full text-left px-3 py-2.5 flex items-start gap-2.5 hover:bg-muted/50 transition-colors border-b border-border/50", selectedInboxThread === agent.id && "bg-muted/60"),
|
|
@@ -125309,6 +125684,40 @@ var TIMELINE_STATUS_LABELS = {
|
|
|
125309
125684
|
var TIMELINE_STATUSES = new Set(Object.keys(TIMELINE_STATUS_LABELS));
|
|
125310
125685
|
var STATUS_DEDUPE_WINDOW_MS = 3e3;
|
|
125311
125686
|
var CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
125687
|
+
var KOKORO_VOICES = [
|
|
125688
|
+
{
|
|
125689
|
+
id: "am_michael",
|
|
125690
|
+
label: "Michael (US ♂)"
|
|
125691
|
+
},
|
|
125692
|
+
{
|
|
125693
|
+
id: "am_adam",
|
|
125694
|
+
label: "Adam (US ♂)"
|
|
125695
|
+
},
|
|
125696
|
+
{
|
|
125697
|
+
id: "af_heart",
|
|
125698
|
+
label: "Heart (US ♀)"
|
|
125699
|
+
},
|
|
125700
|
+
{
|
|
125701
|
+
id: "af_bella",
|
|
125702
|
+
label: "Bella (US ♀)"
|
|
125703
|
+
},
|
|
125704
|
+
{
|
|
125705
|
+
id: "af_nicole",
|
|
125706
|
+
label: "Nicole (US ♀)"
|
|
125707
|
+
},
|
|
125708
|
+
{
|
|
125709
|
+
id: "af_sarah",
|
|
125710
|
+
label: "Sarah (US ♀)"
|
|
125711
|
+
},
|
|
125712
|
+
{
|
|
125713
|
+
id: "bm_george",
|
|
125714
|
+
label: "George (UK ♂)"
|
|
125715
|
+
},
|
|
125716
|
+
{
|
|
125717
|
+
id: "bf_emma",
|
|
125718
|
+
label: "Emma (UK ♀)"
|
|
125719
|
+
}
|
|
125720
|
+
];
|
|
125312
125721
|
function StatusMarker({ event }) {
|
|
125313
125722
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
125314
125723
|
className: "flex items-center justify-center gap-2 py-2 my-1",
|
|
@@ -126342,7 +126751,25 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126342
126751
|
const showError = useRelayStore((s) => s.showError);
|
|
126343
126752
|
const orchestrators = useRelayStore((s) => s.orchestrators);
|
|
126344
126753
|
const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
|
|
126754
|
+
const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
|
|
126755
|
+
const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
|
|
126756
|
+
const voiceTtsLang = useRelayStore((s) => s.voiceTtsLang);
|
|
126757
|
+
const setVoiceTtsLang = useRelayStore((s) => s.setVoiceTtsLang);
|
|
126758
|
+
const voiceTtsMode = useRelayStore((s) => s.voiceTtsMode);
|
|
126759
|
+
const setVoiceTtsMode = useRelayStore((s) => s.setVoiceTtsMode);
|
|
126760
|
+
const voiceTtsKokoroVoice = useRelayStore((s) => s.voiceTtsKokoroVoice);
|
|
126761
|
+
const setVoiceTtsKokoroVoice = useRelayStore((s) => s.setVoiceTtsKokoroVoice);
|
|
126762
|
+
const [speechLangs, setSpeechLangs] = (0, import_react.useState)(() => availableSpeechLangs());
|
|
126763
|
+
(0, import_react.useEffect)(() => {
|
|
126764
|
+
if (!voiceTts.available) return;
|
|
126765
|
+
const refresh = () => setSpeechLangs(availableSpeechLangs());
|
|
126766
|
+
refresh();
|
|
126767
|
+
window.speechSynthesis.addEventListener?.("voiceschanged", refresh);
|
|
126768
|
+
return () => window.speechSynthesis.removeEventListener?.("voiceschanged", refresh);
|
|
126769
|
+
}, []);
|
|
126345
126770
|
const fileInputRef = (0, import_react.useRef)(null);
|
|
126771
|
+
const pttRecorderRef = (0, import_react.useRef)(null);
|
|
126772
|
+
const [micState, setMicState] = (0, import_react.useState)("idle");
|
|
126346
126773
|
const pendingAttachmentsRef = (0, import_react.useRef)([]);
|
|
126347
126774
|
const [pendingAttachments, setPendingAttachments] = (0, import_react.useState)([]);
|
|
126348
126775
|
const [dragActive, setDragActive] = (0, import_react.useState)(false);
|
|
@@ -126633,6 +127060,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126633
127060
|
e.preventDefault();
|
|
126634
127061
|
uploadFiles(files);
|
|
126635
127062
|
}
|
|
127063
|
+
async function startPtt() {
|
|
127064
|
+
if (!micAvailable || micState !== "idle" || !selectedInboxThread) return;
|
|
127065
|
+
voiceTts.bargeIn();
|
|
127066
|
+
const recorder = new PttRecorder();
|
|
127067
|
+
try {
|
|
127068
|
+
await recorder.start();
|
|
127069
|
+
pttRecorderRef.current = recorder;
|
|
127070
|
+
setMicState("recording");
|
|
127071
|
+
} catch (e) {
|
|
127072
|
+
pttRecorderRef.current = null;
|
|
127073
|
+
setMicState("idle");
|
|
127074
|
+
showError("Microphone unavailable", e?.message || "Could not access the microphone.");
|
|
127075
|
+
}
|
|
127076
|
+
}
|
|
127077
|
+
async function stopPtt() {
|
|
127078
|
+
const recorder = pttRecorderRef.current;
|
|
127079
|
+
if (!recorder || micState !== "recording") return;
|
|
127080
|
+
pttRecorderRef.current = null;
|
|
127081
|
+
setMicState("transcribing");
|
|
127082
|
+
try {
|
|
127083
|
+
const clip = await recorder.stop();
|
|
127084
|
+
if (!clip) return;
|
|
127085
|
+
const text = await transcribeClip(clip);
|
|
127086
|
+
if (text) setReplyDraft(selectedInboxThread, draft ? `${draft} ${text}` : text);
|
|
127087
|
+
} catch (e) {
|
|
127088
|
+
showError("Transcription failed", e?.message || "Could not transcribe audio.");
|
|
127089
|
+
} finally {
|
|
127090
|
+
setMicState("idle");
|
|
127091
|
+
}
|
|
127092
|
+
}
|
|
126636
127093
|
if (!selectedInboxThread) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126637
127094
|
className: "flex flex-col items-center justify-center h-full text-center px-4",
|
|
126638
127095
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageSquare, { className: "w-12 h-12 text-zinc-600 mb-3" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
@@ -126699,6 +127156,55 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126699
127156
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
126700
127157
|
className: "flex items-center gap-0.5 md:gap-1 shrink-0",
|
|
126701
127158
|
children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
127159
|
+
voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127160
|
+
variant: "ghost",
|
|
127161
|
+
size: "icon-sm",
|
|
127162
|
+
title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute" : "Speak agent responses aloud (active chat)",
|
|
127163
|
+
className: voiceTtsEnabled ? "text-primary" : "",
|
|
127164
|
+
onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
|
|
127165
|
+
children: voiceTtsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Volume2, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VolumeX, { className: "w-3.5 h-3.5" })
|
|
127166
|
+
}),
|
|
127167
|
+
voiceTts.available && voiceTtsEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
127168
|
+
value: voiceTtsMode,
|
|
127169
|
+
onChange: (e) => setVoiceTtsMode(e.target.value),
|
|
127170
|
+
title: "Voice engine — Kokoro (server, natural) falls back to browser automatically",
|
|
127171
|
+
className: "h-7 rounded border border-border bg-background px-1 text-xs",
|
|
127172
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
127173
|
+
value: "kokoro",
|
|
127174
|
+
children: "Kokoro"
|
|
127175
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
127176
|
+
value: "browser",
|
|
127177
|
+
children: "Browser"
|
|
127178
|
+
})]
|
|
127179
|
+
}), voiceTtsMode === "kokoro" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
|
|
127180
|
+
value: voiceTtsKokoroVoice,
|
|
127181
|
+
onChange: (e) => setVoiceTtsKokoroVoice(e.target.value),
|
|
127182
|
+
title: "Kokoro voice",
|
|
127183
|
+
className: "h-7 rounded border border-border bg-background px-1 text-xs",
|
|
127184
|
+
children: [...KOKORO_VOICES, ...KOKORO_VOICES.some((v) => v.id === voiceTtsKokoroVoice) ? [] : [{
|
|
127185
|
+
id: voiceTtsKokoroVoice,
|
|
127186
|
+
label: voiceTtsKokoroVoice
|
|
127187
|
+
}]].map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
127188
|
+
value: v.id,
|
|
127189
|
+
children: v.label
|
|
127190
|
+
}, v.id))
|
|
127191
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
127192
|
+
value: voiceTtsLang,
|
|
127193
|
+
onChange: (e) => setVoiceTtsLang(e.target.value),
|
|
127194
|
+
title: "Voice language",
|
|
127195
|
+
className: "h-7 rounded border border-border bg-background px-1 text-xs",
|
|
127196
|
+
children: [[...new Set([
|
|
127197
|
+
"en-US",
|
|
127198
|
+
...speechLangs,
|
|
127199
|
+
...voiceTtsLang ? [voiceTtsLang] : []
|
|
127200
|
+
])].sort().map((l) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
127201
|
+
value: l,
|
|
127202
|
+
children: l
|
|
127203
|
+
}, l)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
127204
|
+
value: "",
|
|
127205
|
+
children: "Browser default"
|
|
127206
|
+
})]
|
|
127207
|
+
})] }),
|
|
126702
127208
|
canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126703
127209
|
variant: "ghost",
|
|
126704
127210
|
size: "icon-sm",
|
|
@@ -126960,6 +127466,26 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126960
127466
|
placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
|
|
126961
127467
|
className: "flex-1"
|
|
126962
127468
|
}),
|
|
127469
|
+
micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127470
|
+
variant: "ghost",
|
|
127471
|
+
size: "icon",
|
|
127472
|
+
title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
|
|
127473
|
+
disabled: chatSending || micState === "transcribing",
|
|
127474
|
+
onPointerDown: (e) => {
|
|
127475
|
+
e.preventDefault();
|
|
127476
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
127477
|
+
startPtt();
|
|
127478
|
+
},
|
|
127479
|
+
onPointerUp: (e) => {
|
|
127480
|
+
e.preventDefault();
|
|
127481
|
+
try {
|
|
127482
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
127483
|
+
} catch {}
|
|
127484
|
+
stopPtt();
|
|
127485
|
+
},
|
|
127486
|
+
className: `shrink-0 mb-0.5 rounded-xl h-[42px] w-[42px] ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
|
|
127487
|
+
children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
|
|
127488
|
+
}),
|
|
126963
127489
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126964
127490
|
size: "icon",
|
|
126965
127491
|
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
@@ -126980,14 +127506,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126980
127506
|
className: "w-full"
|
|
126981
127507
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126982
127508
|
className: "flex items-center justify-between",
|
|
126983
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.
|
|
126984
|
-
|
|
126985
|
-
|
|
126986
|
-
|
|
126987
|
-
|
|
126988
|
-
|
|
126989
|
-
|
|
126990
|
-
|
|
127509
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
127510
|
+
className: "flex items-center gap-2",
|
|
127511
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127512
|
+
variant: "ghost",
|
|
127513
|
+
size: "icon",
|
|
127514
|
+
title: "Attach files",
|
|
127515
|
+
disabled: chatSending,
|
|
127516
|
+
onClick: () => fileInputRef.current?.click(),
|
|
127517
|
+
className: "rounded-xl h-9 w-9",
|
|
127518
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Paperclip, { className: "w-4 h-4" })
|
|
127519
|
+
}), micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
127520
|
+
variant: "ghost",
|
|
127521
|
+
size: "icon",
|
|
127522
|
+
title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
|
|
127523
|
+
disabled: chatSending || micState === "transcribing",
|
|
127524
|
+
onPointerDown: (e) => {
|
|
127525
|
+
e.preventDefault();
|
|
127526
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
127527
|
+
startPtt();
|
|
127528
|
+
},
|
|
127529
|
+
onPointerUp: (e) => {
|
|
127530
|
+
e.preventDefault();
|
|
127531
|
+
try {
|
|
127532
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
127533
|
+
} catch {}
|
|
127534
|
+
stopPtt();
|
|
127535
|
+
},
|
|
127536
|
+
className: `rounded-xl h-9 w-9 ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
|
|
127537
|
+
children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
|
|
127538
|
+
})]
|
|
126991
127539
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
126992
127540
|
size: "icon",
|
|
126993
127541
|
disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
|
|
@@ -152306,6 +152854,368 @@ function AnalyticsView() {
|
|
|
152306
152854
|
});
|
|
152307
152855
|
}
|
|
152308
152856
|
//#endregion
|
|
152857
|
+
//#region src/components/views/insights.tsx
|
|
152858
|
+
var DEFAULT_CONFIG = {
|
|
152859
|
+
enabled: true,
|
|
152860
|
+
contextRatio: { enabled: true },
|
|
152861
|
+
introspection: {
|
|
152862
|
+
enabled: true,
|
|
152863
|
+
minTurns: 4,
|
|
152864
|
+
minContextRemaining: .15
|
|
152865
|
+
}
|
|
152866
|
+
};
|
|
152867
|
+
function compactJson(value) {
|
|
152868
|
+
if (!value || Object.keys(value).length === 0) return "—";
|
|
152869
|
+
return Object.entries(value).map(([k, v]) => `${k}: ${typeof v === "number" ? Math.round(v * 1e3) / 1e3 : String(v)}`).join(" ");
|
|
152870
|
+
}
|
|
152871
|
+
function InsightsView() {
|
|
152872
|
+
const [config, setConfig] = (0, import_react.useState)(DEFAULT_CONFIG);
|
|
152873
|
+
const [version, setVersion] = (0, import_react.useState)(0);
|
|
152874
|
+
const [observations, setObservations] = (0, import_react.useState)([]);
|
|
152875
|
+
const [stats, setStats] = (0, import_react.useState)([]);
|
|
152876
|
+
const [projects, setProjects] = (0, import_react.useState)([]);
|
|
152877
|
+
const [projectFilter, setProjectFilter] = (0, import_react.useState)("");
|
|
152878
|
+
const [signalFilter, setSignalFilter] = (0, import_react.useState)("");
|
|
152879
|
+
const [saving, setSaving] = (0, import_react.useState)(false);
|
|
152880
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
152881
|
+
const [now, setNow] = (0, import_react.useState)(() => Date.now());
|
|
152882
|
+
async function refresh() {
|
|
152883
|
+
setError(null);
|
|
152884
|
+
try {
|
|
152885
|
+
const params = new URLSearchParams();
|
|
152886
|
+
if (projectFilter) params.set("project", projectFilter);
|
|
152887
|
+
if (signalFilter) params.set("signal", signalFilter);
|
|
152888
|
+
const query = params.toString();
|
|
152889
|
+
const [cfg, obs] = await Promise.all([api("GET", "/insights/config"), api("GET", `/insights/observations${query ? `?${query}` : ""}`)]);
|
|
152890
|
+
setConfig(cfg.value);
|
|
152891
|
+
setVersion(cfg.version);
|
|
152892
|
+
setObservations(obs.observations);
|
|
152893
|
+
setStats(obs.stats);
|
|
152894
|
+
setProjects(obs.projects);
|
|
152895
|
+
setNow(Date.now());
|
|
152896
|
+
} catch (e) {
|
|
152897
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
152898
|
+
}
|
|
152899
|
+
}
|
|
152900
|
+
(0, import_react.useEffect)(() => {
|
|
152901
|
+
refresh();
|
|
152902
|
+
}, [projectFilter, signalFilter]);
|
|
152903
|
+
async function saveConfig(next) {
|
|
152904
|
+
setSaving(true);
|
|
152905
|
+
setError(null);
|
|
152906
|
+
const previous = config;
|
|
152907
|
+
setConfig(next);
|
|
152908
|
+
try {
|
|
152909
|
+
const entry = await api("PUT", "/insights/config", {
|
|
152910
|
+
value: next,
|
|
152911
|
+
updatedBy: "dashboard"
|
|
152912
|
+
});
|
|
152913
|
+
setConfig(entry.value);
|
|
152914
|
+
setVersion(entry.version);
|
|
152915
|
+
} catch (e) {
|
|
152916
|
+
setConfig(previous);
|
|
152917
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
152918
|
+
} finally {
|
|
152919
|
+
setSaving(false);
|
|
152920
|
+
}
|
|
152921
|
+
}
|
|
152922
|
+
const signals = (0, import_react.useMemo)(() => {
|
|
152923
|
+
return [...new Set(stats.map((s) => s.signal))].sort();
|
|
152924
|
+
}, [stats]);
|
|
152925
|
+
const globalStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project === null), [stats]);
|
|
152926
|
+
const projectStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project !== null), [stats]);
|
|
152927
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152928
|
+
className: "space-y-4 p-4",
|
|
152929
|
+
children: [
|
|
152930
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152931
|
+
className: "flex items-center justify-between",
|
|
152932
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152933
|
+
className: "flex items-center gap-2",
|
|
152934
|
+
children: [
|
|
152935
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Lightbulb, { className: "size-5 text-primary" }),
|
|
152936
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
|
|
152937
|
+
className: "text-lg font-semibold",
|
|
152938
|
+
children: "Insights"
|
|
152939
|
+
}),
|
|
152940
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
152941
|
+
className: "text-xs text-muted-foreground",
|
|
152942
|
+
children: "continuous self-improvement · epic #183"
|
|
152943
|
+
})
|
|
152944
|
+
]
|
|
152945
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
152946
|
+
variant: "outline",
|
|
152947
|
+
size: "sm",
|
|
152948
|
+
onClick: () => void refresh(),
|
|
152949
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { className: "size-4" }), " Refresh"]
|
|
152950
|
+
})]
|
|
152951
|
+
}),
|
|
152952
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152953
|
+
className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
|
|
152954
|
+
children: error
|
|
152955
|
+
}),
|
|
152956
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
|
|
152957
|
+
className: "flex items-center justify-between",
|
|
152958
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Features" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
152959
|
+
className: "text-xs font-normal text-muted-foreground",
|
|
152960
|
+
children: [
|
|
152961
|
+
"config v",
|
|
152962
|
+
version,
|
|
152963
|
+
" · saved by ",
|
|
152964
|
+
config && "dashboard"
|
|
152965
|
+
]
|
|
152966
|
+
})]
|
|
152967
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
|
|
152968
|
+
className: "space-y-4",
|
|
152969
|
+
children: [
|
|
152970
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152971
|
+
className: "flex items-center justify-between",
|
|
152972
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152973
|
+
className: "text-sm font-medium",
|
|
152974
|
+
children: "Insights enabled"
|
|
152975
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152976
|
+
className: "text-xs text-muted-foreground",
|
|
152977
|
+
children: "Master switch. When off, no signals are recorded regardless of the toggles below."
|
|
152978
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
152979
|
+
checked: config.enabled,
|
|
152980
|
+
disabled: saving,
|
|
152981
|
+
onCheckedChange: (v) => void saveConfig({
|
|
152982
|
+
...config,
|
|
152983
|
+
enabled: v
|
|
152984
|
+
})
|
|
152985
|
+
})]
|
|
152986
|
+
}),
|
|
152987
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152988
|
+
className: "flex items-center justify-between border-t border-border pt-3",
|
|
152989
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152990
|
+
className: "text-sm font-medium",
|
|
152991
|
+
children: ["Context-gathering ratio ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
152992
|
+
className: "text-xs text-muted-foreground",
|
|
152993
|
+
children: "(#184)"
|
|
152994
|
+
})]
|
|
152995
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152996
|
+
className: "text-xs text-muted-foreground",
|
|
152997
|
+
children: "Server-side: read/search vs. action before first substantive move, per session."
|
|
152998
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
152999
|
+
checked: config.contextRatio.enabled,
|
|
153000
|
+
disabled: saving || !config.enabled,
|
|
153001
|
+
onCheckedChange: (v) => void saveConfig({
|
|
153002
|
+
...config,
|
|
153003
|
+
contextRatio: { enabled: v }
|
|
153004
|
+
})
|
|
153005
|
+
})]
|
|
153006
|
+
}),
|
|
153007
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153008
|
+
className: "border-t border-border pt-3",
|
|
153009
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153010
|
+
className: "flex items-center justify-between",
|
|
153011
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153012
|
+
className: "text-sm font-medium",
|
|
153013
|
+
children: ["End-of-session introspection ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
153014
|
+
className: "text-xs text-muted-foreground",
|
|
153015
|
+
children: "(#185)"
|
|
153016
|
+
})]
|
|
153017
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153018
|
+
className: "text-xs text-muted-foreground",
|
|
153019
|
+
children: "Agent-authored 3-field artifact at session end. Gated below."
|
|
153020
|
+
})] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
153021
|
+
checked: config.introspection.enabled,
|
|
153022
|
+
disabled: saving || !config.enabled,
|
|
153023
|
+
onCheckedChange: (v) => void saveConfig({
|
|
153024
|
+
...config,
|
|
153025
|
+
introspection: {
|
|
153026
|
+
...config.introspection,
|
|
153027
|
+
enabled: v
|
|
153028
|
+
}
|
|
153029
|
+
})
|
|
153030
|
+
})]
|
|
153031
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153032
|
+
className: "mt-3 flex flex-wrap gap-4",
|
|
153033
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
153034
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
153035
|
+
children: ["min turns", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
153036
|
+
type: "number",
|
|
153037
|
+
className: "h-7 w-20",
|
|
153038
|
+
value: config.introspection.minTurns,
|
|
153039
|
+
disabled: saving || !config.enabled || !config.introspection.enabled,
|
|
153040
|
+
onChange: (e) => setConfig({
|
|
153041
|
+
...config,
|
|
153042
|
+
introspection: {
|
|
153043
|
+
...config.introspection,
|
|
153044
|
+
minTurns: Number(e.target.value)
|
|
153045
|
+
}
|
|
153046
|
+
}),
|
|
153047
|
+
onBlur: () => void saveConfig(config)
|
|
153048
|
+
})]
|
|
153049
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
|
|
153050
|
+
className: "flex items-center gap-2 text-xs text-muted-foreground",
|
|
153051
|
+
children: ["min context remaining (0–1)", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
153052
|
+
type: "number",
|
|
153053
|
+
step: "0.05",
|
|
153054
|
+
min: "0",
|
|
153055
|
+
max: "1",
|
|
153056
|
+
className: "h-7 w-24",
|
|
153057
|
+
value: config.introspection.minContextRemaining,
|
|
153058
|
+
disabled: saving || !config.enabled || !config.introspection.enabled,
|
|
153059
|
+
onChange: (e) => setConfig({
|
|
153060
|
+
...config,
|
|
153061
|
+
introspection: {
|
|
153062
|
+
...config.introspection,
|
|
153063
|
+
minContextRemaining: Number(e.target.value)
|
|
153064
|
+
}
|
|
153065
|
+
}),
|
|
153066
|
+
onBlur: () => void saveConfig(config)
|
|
153067
|
+
})]
|
|
153068
|
+
})]
|
|
153069
|
+
})]
|
|
153070
|
+
})
|
|
153071
|
+
]
|
|
153072
|
+
})] }),
|
|
153073
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, { children: "Signals" }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: globalStats.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153074
|
+
className: "text-sm text-muted-foreground",
|
|
153075
|
+
children: "No observations yet — they appear after sessions end with Insights enabled."
|
|
153076
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153077
|
+
className: "space-y-4",
|
|
153078
|
+
children: globalStats.map((g) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153079
|
+
className: "mb-1 flex items-center gap-2",
|
|
153080
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
153081
|
+
variant: "secondary",
|
|
153082
|
+
children: g.signal
|
|
153083
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
153084
|
+
className: "text-xs text-muted-foreground",
|
|
153085
|
+
children: [
|
|
153086
|
+
g.count,
|
|
153087
|
+
" obs · ",
|
|
153088
|
+
g.avgRatio !== null ? `avg ratio ${g.avgRatio.toFixed(2)}` : "no ratio",
|
|
153089
|
+
" · ",
|
|
153090
|
+
timeAgo(now, g.lastAt)
|
|
153091
|
+
]
|
|
153092
|
+
})]
|
|
153093
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153094
|
+
className: "flex flex-wrap gap-2 pl-2",
|
|
153095
|
+
children: projectStats.filter((p) => p.signal === g.signal).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
153096
|
+
className: "rounded border border-border px-2 py-0.5 text-xs text-muted-foreground",
|
|
153097
|
+
children: [
|
|
153098
|
+
p.project,
|
|
153099
|
+
": ",
|
|
153100
|
+
p.count,
|
|
153101
|
+
p.avgRatio !== null ? ` · ${p.avgRatio.toFixed(2)}` : ""
|
|
153102
|
+
]
|
|
153103
|
+
}, `${g.signal}-${p.project}`))
|
|
153104
|
+
})] }, g.signal))
|
|
153105
|
+
}) })] }),
|
|
153106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
|
|
153107
|
+
className: "flex flex-wrap items-center gap-3",
|
|
153108
|
+
children: [
|
|
153109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Observations" }),
|
|
153110
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
153111
|
+
className: "h-7 rounded border border-border bg-background px-2 text-xs",
|
|
153112
|
+
value: projectFilter,
|
|
153113
|
+
onChange: (e) => setProjectFilter(e.target.value),
|
|
153114
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
153115
|
+
value: "",
|
|
153116
|
+
children: "all projects"
|
|
153117
|
+
}), projects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
153118
|
+
value: p,
|
|
153119
|
+
children: p
|
|
153120
|
+
}, p))]
|
|
153121
|
+
}),
|
|
153122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
|
|
153123
|
+
className: "h-7 rounded border border-border bg-background px-2 text-xs",
|
|
153124
|
+
value: signalFilter,
|
|
153125
|
+
onChange: (e) => setSignalFilter(e.target.value),
|
|
153126
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
153127
|
+
value: "",
|
|
153128
|
+
children: "all signals"
|
|
153129
|
+
}), signals.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
|
|
153130
|
+
value: s,
|
|
153131
|
+
children: s
|
|
153132
|
+
}, s))]
|
|
153133
|
+
})
|
|
153134
|
+
]
|
|
153135
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: observations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153136
|
+
className: "text-sm text-muted-foreground",
|
|
153137
|
+
children: "No observations match."
|
|
153138
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
153139
|
+
className: "overflow-x-auto",
|
|
153140
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
|
|
153141
|
+
className: "w-full text-left text-xs",
|
|
153142
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
|
|
153143
|
+
className: "text-muted-foreground",
|
|
153144
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
|
|
153145
|
+
className: "border-b border-border",
|
|
153146
|
+
children: [
|
|
153147
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153148
|
+
className: "py-1 pr-3 font-medium",
|
|
153149
|
+
children: "when"
|
|
153150
|
+
}),
|
|
153151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153152
|
+
className: "py-1 pr-3 font-medium",
|
|
153153
|
+
children: "project"
|
|
153154
|
+
}),
|
|
153155
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153156
|
+
className: "py-1 pr-3 font-medium",
|
|
153157
|
+
children: "signal"
|
|
153158
|
+
}),
|
|
153159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153160
|
+
className: "py-1 pr-3 font-medium",
|
|
153161
|
+
children: "src"
|
|
153162
|
+
}),
|
|
153163
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153164
|
+
className: "py-1 pr-3 font-medium",
|
|
153165
|
+
children: "value"
|
|
153166
|
+
}),
|
|
153167
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153168
|
+
className: "py-1 pr-3 font-medium",
|
|
153169
|
+
children: "outcome"
|
|
153170
|
+
}),
|
|
153171
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
|
|
153172
|
+
className: "py-1 pr-3 font-medium",
|
|
153173
|
+
children: "session"
|
|
153174
|
+
})
|
|
153175
|
+
]
|
|
153176
|
+
})
|
|
153177
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: observations.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
|
|
153178
|
+
className: "border-b border-border/50",
|
|
153179
|
+
children: [
|
|
153180
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153181
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
153182
|
+
children: timeAgo(now, o.createdAt)
|
|
153183
|
+
}),
|
|
153184
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153185
|
+
className: "py-1 pr-3 whitespace-nowrap",
|
|
153186
|
+
children: o.project
|
|
153187
|
+
}),
|
|
153188
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153189
|
+
className: "py-1 pr-3 whitespace-nowrap",
|
|
153190
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
|
|
153191
|
+
variant: "outline",
|
|
153192
|
+
children: o.signal
|
|
153193
|
+
})
|
|
153194
|
+
}),
|
|
153195
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153196
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
153197
|
+
children: o.source
|
|
153198
|
+
}),
|
|
153199
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153200
|
+
className: "py-1 pr-3 font-mono",
|
|
153201
|
+
children: compactJson(o.value)
|
|
153202
|
+
}),
|
|
153203
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153204
|
+
className: "py-1 pr-3 font-mono text-muted-foreground",
|
|
153205
|
+
children: compactJson(o.outcome)
|
|
153206
|
+
}),
|
|
153207
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
|
|
153208
|
+
className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
|
|
153209
|
+
children: o.agentId ?? o.sessionId.slice(0, 12)
|
|
153210
|
+
})
|
|
153211
|
+
]
|
|
153212
|
+
}, o.id)) })]
|
|
153213
|
+
})
|
|
153214
|
+
}) })] })
|
|
153215
|
+
]
|
|
153216
|
+
});
|
|
153217
|
+
}
|
|
153218
|
+
//#endregion
|
|
152309
153219
|
//#region src/components/views/maintenance.tsx
|
|
152310
153220
|
var STATUS_CLASS = {
|
|
152311
153221
|
succeeded: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
|
|
@@ -156885,6 +157795,7 @@ var views = {
|
|
|
156885
157795
|
tasks: TasksView,
|
|
156886
157796
|
automation: AutomationView,
|
|
156887
157797
|
analytics: AnalyticsView,
|
|
157798
|
+
insights: InsightsView,
|
|
156888
157799
|
maintenance: MaintenanceView,
|
|
156889
157800
|
settings: SettingsView
|
|
156890
157801
|
};
|
|
@@ -157903,6 +158814,11 @@ if ("serviceWorker" in navigator) {
|
|
|
157903
158814
|
height: calc(var(--spacing) * 4);
|
|
157904
158815
|
}
|
|
157905
158816
|
|
|
158817
|
+
.size-5 {
|
|
158818
|
+
width: calc(var(--spacing) * 5);
|
|
158819
|
+
height: calc(var(--spacing) * 5);
|
|
158820
|
+
}
|
|
158821
|
+
|
|
157906
158822
|
.size-6 {
|
|
157907
158823
|
width: calc(var(--spacing) * 6);
|
|
157908
158824
|
height: calc(var(--spacing) * 6);
|
|
@@ -158224,6 +159140,14 @@ if ("serviceWorker" in navigator) {
|
|
|
158224
159140
|
width: calc(var(--spacing) * 14);
|
|
158225
159141
|
}
|
|
158226
159142
|
|
|
159143
|
+
.w-20 {
|
|
159144
|
+
width: calc(var(--spacing) * 20);
|
|
159145
|
+
}
|
|
159146
|
+
|
|
159147
|
+
.w-24 {
|
|
159148
|
+
width: calc(var(--spacing) * 24);
|
|
159149
|
+
}
|
|
159150
|
+
|
|
158227
159151
|
.w-40 {
|
|
158228
159152
|
width: calc(var(--spacing) * 40);
|
|
158229
159153
|
}
|
|
@@ -160132,6 +161056,10 @@ if ("serviceWorker" in navigator) {
|
|
|
160132
161056
|
padding-top: calc(var(--spacing) * 2);
|
|
160133
161057
|
}
|
|
160134
161058
|
|
|
161059
|
+
.pt-3 {
|
|
161060
|
+
padding-top: calc(var(--spacing) * 3);
|
|
161061
|
+
}
|
|
161062
|
+
|
|
160135
161063
|
.pt-4 {
|
|
160136
161064
|
padding-top: calc(var(--spacing) * 4);
|
|
160137
161065
|
}
|
|
@@ -160144,6 +161072,10 @@ if ("serviceWorker" in navigator) {
|
|
|
160144
161072
|
padding-right: calc(var(--spacing) * 2);
|
|
160145
161073
|
}
|
|
160146
161074
|
|
|
161075
|
+
.pr-3 {
|
|
161076
|
+
padding-right: calc(var(--spacing) * 3);
|
|
161077
|
+
}
|
|
161078
|
+
|
|
160147
161079
|
.pr-4 {
|
|
160148
161080
|
padding-right: calc(var(--spacing) * 4);
|
|
160149
161081
|
}
|