@yieldfabric/terminal 1.1.2 → 1.3.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/index.cjs +1274 -12
- package/dist/index.d.cts +423 -3
- package/dist/index.d.ts +423 -3
- package/dist/index.js +1260 -14
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -818,11 +818,11 @@ var AVATAR_COLORS = [
|
|
|
818
818
|
"#ef4444"
|
|
819
819
|
];
|
|
820
820
|
function avatarColor(entityId) {
|
|
821
|
-
let
|
|
821
|
+
let hash2 = 0;
|
|
822
822
|
for (let i = 0; i < entityId.length; i++) {
|
|
823
|
-
|
|
823
|
+
hash2 = entityId.charCodeAt(i) + ((hash2 << 5) - hash2);
|
|
824
824
|
}
|
|
825
|
-
return AVATAR_COLORS[Math.abs(
|
|
825
|
+
return AVATAR_COLORS[Math.abs(hash2) % AVATAR_COLORS.length];
|
|
826
826
|
}
|
|
827
827
|
function initials(name) {
|
|
828
828
|
return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
|
|
@@ -2234,18 +2234,21 @@ function CopyButton({ content }) {
|
|
|
2234
2234
|
}
|
|
2235
2235
|
);
|
|
2236
2236
|
}
|
|
2237
|
+
var MESSAGES = {
|
|
2238
|
+
rounds_exhausted: "Partial answer \u2014 I reached the step limit. Ask a narrower follow-up to continue.",
|
|
2239
|
+
budget_exhausted: "Partial answer \u2014 this question needs more context than fits in one answer. Try narrowing the question.",
|
|
2240
|
+
deadline: "Partial answer \u2014 I ran out of time before finishing. Ask a narrower follow-up to continue.",
|
|
2241
|
+
output_limit: "Partial answer \u2014 it was cut off at its length limit. Ask for the rest, or a narrower follow-up.",
|
|
2242
|
+
awaiting_tool_results: "This answer paused before its next step. Ask again to review a fresh answer.",
|
|
2243
|
+
empty: "No answer was returned. Please try again.",
|
|
2244
|
+
failed: "The answer could not be completed. What arrived so far is kept here.",
|
|
2245
|
+
interrupted: "Connection interrupted. The assistant may still be working; check the conversation before sending again.",
|
|
2246
|
+
cancelled: "Stopped. You can change your question and ask again."
|
|
2247
|
+
};
|
|
2237
2248
|
function AgentCompletionNotice({ completion }) {
|
|
2238
2249
|
const t = useTheme();
|
|
2239
2250
|
if (!completion || completion.stop_reason === "answered") return null;
|
|
2240
|
-
const message =
|
|
2241
|
-
rounds_exhausted: "Partial answer \u2014 I reached the step limit. Ask a narrower follow-up to continue.",
|
|
2242
|
-
budget_exhausted: "Partial answer \u2014 this question needs more context than fits in one answer. Try narrowing the question.",
|
|
2243
|
-
awaiting_tool_results: "This answer paused before its next step. Ask again to review a fresh answer.",
|
|
2244
|
-
empty: "No answer was returned. Please try again.",
|
|
2245
|
-
failed: "The answer could not be completed. What arrived so far is kept here.",
|
|
2246
|
-
interrupted: "Connection interrupted. The assistant may still be working; check the conversation before sending again.",
|
|
2247
|
-
cancelled: "Stopped. You can change your question and ask again."
|
|
2248
|
-
}[completion.stop_reason];
|
|
2251
|
+
const message = MESSAGES[completion.stop_reason];
|
|
2249
2252
|
if (!message) return null;
|
|
2250
2253
|
return /* @__PURE__ */ jsxRuntime.jsx("p", { role: "status", className: "mt-2 rounded-md px-3 py-2 text-xs", style: { color: t.textSecondary, backgroundColor: t.bgRaisedAlt, border: `1px solid ${t.border}` }, children: message });
|
|
2251
2254
|
}
|
|
@@ -3389,6 +3392,7 @@ function TerminalInputBar({
|
|
|
3389
3392
|
selectedPaletteItem,
|
|
3390
3393
|
onSelectPaletteItem,
|
|
3391
3394
|
inputBarExtra,
|
|
3395
|
+
leading,
|
|
3392
3396
|
modeSelector,
|
|
3393
3397
|
compactToolbar = false,
|
|
3394
3398
|
submitTitle = "Send",
|
|
@@ -3667,6 +3671,7 @@ function TerminalInputBar({
|
|
|
3667
3671
|
})(),
|
|
3668
3672
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { ref: toolbarRef, className: "flex items-center justify-between pt-2.5 gap-2", children: [
|
|
3669
3673
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-[11px] min-w-0 flex-wrap", children: [
|
|
3674
|
+
leading,
|
|
3670
3675
|
chipsVisible && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3671
3676
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3672
3677
|
"button",
|
|
@@ -5547,6 +5552,7 @@ Path: ${parsed.endpoint.path}`);
|
|
|
5547
5552
|
core.setCommandPaletteItems([]);
|
|
5548
5553
|
},
|
|
5549
5554
|
inputBarExtra: slots?.inputBarExtra,
|
|
5555
|
+
leading: slots?.inputBarLeading,
|
|
5550
5556
|
modeSelector: slots?.modeSelector
|
|
5551
5557
|
}
|
|
5552
5558
|
),
|
|
@@ -6219,6 +6225,7 @@ function ChatTerminal({
|
|
|
6219
6225
|
core.setCommandPaletteItems([]);
|
|
6220
6226
|
},
|
|
6221
6227
|
inputBarExtra: slots?.inputBarExtra,
|
|
6228
|
+
leading: slots?.inputBarLeading,
|
|
6222
6229
|
modeSelector: slots?.modeSelector
|
|
6223
6230
|
}
|
|
6224
6231
|
),
|
|
@@ -7622,6 +7629,1245 @@ function agentStepTraceExtra(entry) {
|
|
|
7622
7629
|
const working = meta?.agentStepsLive ?? entry.content.endsWith("\u258B");
|
|
7623
7630
|
return /* @__PURE__ */ jsxRuntime.jsx(AgentStepTrace, { steps, working });
|
|
7624
7631
|
}
|
|
7632
|
+
var BAR_SHAPE = [0.55, 1, 0.8, 0.45];
|
|
7633
|
+
function tintOf(color, alpha) {
|
|
7634
|
+
return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`;
|
|
7635
|
+
}
|
|
7636
|
+
function useCoarsePointer() {
|
|
7637
|
+
const [coarse, setCoarse] = React15.useState(
|
|
7638
|
+
() => typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches
|
|
7639
|
+
);
|
|
7640
|
+
React15.useEffect(() => {
|
|
7641
|
+
const mq = window.matchMedia?.("(pointer: coarse)");
|
|
7642
|
+
if (!mq) return void 0;
|
|
7643
|
+
const on = () => setCoarse(mq.matches);
|
|
7644
|
+
mq.addEventListener?.("change", on);
|
|
7645
|
+
return () => mq.removeEventListener?.("change", on);
|
|
7646
|
+
}, []);
|
|
7647
|
+
return coarse;
|
|
7648
|
+
}
|
|
7649
|
+
function VoiceInputButton({
|
|
7650
|
+
state,
|
|
7651
|
+
onToggle,
|
|
7652
|
+
disabled = false,
|
|
7653
|
+
level,
|
|
7654
|
+
startTitle = "Talk",
|
|
7655
|
+
stopTitle = "End voice"
|
|
7656
|
+
}) {
|
|
7657
|
+
const t = useTheme();
|
|
7658
|
+
const coarse = useCoarsePointer();
|
|
7659
|
+
const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
|
|
7660
|
+
const on = state !== "off";
|
|
7661
|
+
const tint = (a) => tintOf(t.primary, a);
|
|
7662
|
+
const bars = React15.useRef([]);
|
|
7663
|
+
const levelRef = React15.useRef(level);
|
|
7664
|
+
levelRef.current = level;
|
|
7665
|
+
React15.useEffect(() => {
|
|
7666
|
+
if (state !== "live") return void 0;
|
|
7667
|
+
let raf = 0;
|
|
7668
|
+
let smooth = 0;
|
|
7669
|
+
const tick = (now) => {
|
|
7670
|
+
const raw = levelRef.current ? Math.min(1, Math.max(0, levelRef.current())) : 0;
|
|
7671
|
+
smooth = raw > smooth ? smooth + (raw - smooth) * 0.55 : smooth + (raw - smooth) * 0.12;
|
|
7672
|
+
bars.current.forEach((bar, i) => {
|
|
7673
|
+
if (!bar) return;
|
|
7674
|
+
const idle = 0.18 + 0.1 * Math.sin(now / 320 + i * 1.3);
|
|
7675
|
+
const h = Math.max(idle, smooth * BAR_SHAPE[i] * (0.85 + 0.15 * Math.sin(now / 90 + i * 2.1)));
|
|
7676
|
+
bar.style.transform = `scaleY(${Math.min(1, h).toFixed(3)})`;
|
|
7677
|
+
});
|
|
7678
|
+
raf = window.requestAnimationFrame(tick);
|
|
7679
|
+
};
|
|
7680
|
+
raf = window.requestAnimationFrame(tick);
|
|
7681
|
+
return () => window.cancelAnimationFrame(raf);
|
|
7682
|
+
}, [state]);
|
|
7683
|
+
const size = coarse ? 32 : 22;
|
|
7684
|
+
const title = on ? stopTitle : startTitle;
|
|
7685
|
+
const style = on ? {
|
|
7686
|
+
backgroundColor: t.primary,
|
|
7687
|
+
border: `1px solid ${t.primary}`,
|
|
7688
|
+
color: t.textInverse,
|
|
7689
|
+
boxShadow: `0 0 0 3px ${tint(0.18)}, 0 6px 18px -6px ${tint(0.75)}`
|
|
7690
|
+
} : {
|
|
7691
|
+
backgroundColor: isLight ? tint(0.07) : tint(0.1),
|
|
7692
|
+
border: `1px solid ${tint(isLight ? 0.35 : 0.3)}`,
|
|
7693
|
+
color: t.primary
|
|
7694
|
+
};
|
|
7695
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7696
|
+
"button",
|
|
7697
|
+
{
|
|
7698
|
+
type: "button",
|
|
7699
|
+
disabled: disabled && !on,
|
|
7700
|
+
"aria-pressed": on,
|
|
7701
|
+
"aria-label": title,
|
|
7702
|
+
title,
|
|
7703
|
+
className: `relative flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-200 active:scale-95 ${coarse ? "rounded-full" : "rounded-lg px-1.5"}`,
|
|
7704
|
+
style: { ...style, height: size, minWidth: size, WebkitTapHighlightColor: "transparent" },
|
|
7705
|
+
onClick: onToggle,
|
|
7706
|
+
children: [
|
|
7707
|
+
state === "live" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
7708
|
+
"span",
|
|
7709
|
+
{
|
|
7710
|
+
"aria-hidden": "true",
|
|
7711
|
+
className: `pointer-events-none absolute inset-0 animate-ping ${coarse ? "rounded-full" : "rounded-lg"}`,
|
|
7712
|
+
style: { backgroundColor: tint(0.35), animationDuration: "1.8s" }
|
|
7713
|
+
}
|
|
7714
|
+
) : null,
|
|
7715
|
+
state === "connecting" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: coarse ? "w-4 h-4 animate-spin" : "w-3.5 h-3.5 animate-spin" }) : state === "live" ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", className: "relative flex items-center gap-[2px]", style: { height: coarse ? 14 : 11 }, children: BAR_SHAPE.map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
7716
|
+
"span",
|
|
7717
|
+
{
|
|
7718
|
+
ref: (el) => {
|
|
7719
|
+
bars.current[i] = el;
|
|
7720
|
+
},
|
|
7721
|
+
className: "block rounded-full",
|
|
7722
|
+
style: {
|
|
7723
|
+
width: coarse ? 2.5 : 2,
|
|
7724
|
+
height: "100%",
|
|
7725
|
+
backgroundColor: "currentColor",
|
|
7726
|
+
transform: "scaleY(0.25)",
|
|
7727
|
+
transformOrigin: "center",
|
|
7728
|
+
transition: "transform 60ms linear"
|
|
7729
|
+
}
|
|
7730
|
+
},
|
|
7731
|
+
i
|
|
7732
|
+
)) }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: coarse ? "w-4 h-4" : "w-3.5 h-3.5" })
|
|
7733
|
+
]
|
|
7734
|
+
}
|
|
7735
|
+
);
|
|
7736
|
+
}
|
|
7737
|
+
|
|
7738
|
+
// src/voice/askQueue.ts
|
|
7739
|
+
var REPLACED_OUTPUT = "Replaced by their corrected question, which is being answered instead. Say nothing about this one.";
|
|
7740
|
+
var AskQueue = class {
|
|
7741
|
+
/**
|
|
7742
|
+
* `run` asks the host (resolves with what the voice speaks from; a
|
|
7743
|
+
* rejection is reported as a failure). `finish` sends the result: `speak`
|
|
7744
|
+
* false for a replaced question.
|
|
7745
|
+
*/
|
|
7746
|
+
constructor(run, finish) {
|
|
7747
|
+
this.run = run;
|
|
7748
|
+
this.finish = finish;
|
|
7749
|
+
this.waiting = [];
|
|
7750
|
+
this.running = null;
|
|
7751
|
+
}
|
|
7752
|
+
/** Questions being worked on or waiting. */
|
|
7753
|
+
get size() {
|
|
7754
|
+
return this.waiting.length + (this.running ? 1 : 0);
|
|
7755
|
+
}
|
|
7756
|
+
add(ask) {
|
|
7757
|
+
if (ask.replaces && this.size > 0) {
|
|
7758
|
+
for (const dropped of this.waiting) this.finish(dropped, REPLACED_OUTPUT, false);
|
|
7759
|
+
this.waiting = [ask];
|
|
7760
|
+
this.running?.controller.abort();
|
|
7761
|
+
} else {
|
|
7762
|
+
this.waiting.push(ask);
|
|
7763
|
+
}
|
|
7764
|
+
this.next();
|
|
7765
|
+
}
|
|
7766
|
+
next() {
|
|
7767
|
+
if (this.running) return;
|
|
7768
|
+
const ask = this.waiting.shift();
|
|
7769
|
+
if (!ask) return;
|
|
7770
|
+
const controller = new AbortController();
|
|
7771
|
+
this.running = { ask, controller };
|
|
7772
|
+
this.run(ask, controller.signal).catch((e) => `The assistant could not answer: ${e?.message || "unknown error"}. Tell them it failed; do not guess.`).then((output) => {
|
|
7773
|
+
this.running = null;
|
|
7774
|
+
if (controller.signal.aborted) this.finish(ask, REPLACED_OUTPUT, false);
|
|
7775
|
+
else this.finish(ask, output, true);
|
|
7776
|
+
this.next();
|
|
7777
|
+
});
|
|
7778
|
+
}
|
|
7779
|
+
};
|
|
7780
|
+
|
|
7781
|
+
// src/voice/pcm.ts
|
|
7782
|
+
var WIRE_RATE = 24e3;
|
|
7783
|
+
var FRAME_SAMPLES = 2400;
|
|
7784
|
+
var Downsampler = class {
|
|
7785
|
+
constructor(inputRate, outputRate = WIRE_RATE) {
|
|
7786
|
+
/** Input samples not yet consumed. */
|
|
7787
|
+
this.carry = new Float32Array(0);
|
|
7788
|
+
/** Fractional read position into `carry`. */
|
|
7789
|
+
this.pos = 0;
|
|
7790
|
+
if (!(inputRate > 0) || !(outputRate > 0)) throw new Error("Sample rates must be positive");
|
|
7791
|
+
this.ratio = inputRate / outputRate;
|
|
7792
|
+
}
|
|
7793
|
+
push(input) {
|
|
7794
|
+
const buf = new Float32Array(this.carry.length + input.length);
|
|
7795
|
+
buf.set(this.carry, 0);
|
|
7796
|
+
buf.set(input, this.carry.length);
|
|
7797
|
+
const out = [];
|
|
7798
|
+
let pos = this.pos;
|
|
7799
|
+
while (pos + this.ratio <= buf.length) {
|
|
7800
|
+
const start = Math.floor(pos);
|
|
7801
|
+
const end = Math.max(start + 1, Math.floor(pos + this.ratio));
|
|
7802
|
+
let sum = 0;
|
|
7803
|
+
for (let i = start; i < end; i += 1) sum += buf[i];
|
|
7804
|
+
out.push(floatToInt16(sum / (end - start)));
|
|
7805
|
+
pos += this.ratio;
|
|
7806
|
+
}
|
|
7807
|
+
const consumed = Math.floor(pos);
|
|
7808
|
+
this.carry = buf.slice(consumed);
|
|
7809
|
+
this.pos = pos - consumed;
|
|
7810
|
+
return Int16Array.from(out);
|
|
7811
|
+
}
|
|
7812
|
+
};
|
|
7813
|
+
function floatToInt16(sample) {
|
|
7814
|
+
const s = Math.max(-1, Math.min(1, Number.isFinite(sample) ? sample : 0));
|
|
7815
|
+
return s < 0 ? Math.round(s * 32768) : Math.round(s * 32767);
|
|
7816
|
+
}
|
|
7817
|
+
var FrameBuffer = class {
|
|
7818
|
+
constructor() {
|
|
7819
|
+
this.pending = [];
|
|
7820
|
+
}
|
|
7821
|
+
push(samples) {
|
|
7822
|
+
for (let i = 0; i < samples.length; i += 1) this.pending.push(samples[i]);
|
|
7823
|
+
const frames = [];
|
|
7824
|
+
while (this.pending.length >= FRAME_SAMPLES) {
|
|
7825
|
+
frames.push(toLittleEndian(this.pending.splice(0, FRAME_SAMPLES)));
|
|
7826
|
+
}
|
|
7827
|
+
return frames;
|
|
7828
|
+
}
|
|
7829
|
+
};
|
|
7830
|
+
function toLittleEndian(samples) {
|
|
7831
|
+
const view = new DataView(new ArrayBuffer(samples.length * 2));
|
|
7832
|
+
samples.forEach((s, i) => view.setInt16(i * 2, s, true));
|
|
7833
|
+
return view.buffer;
|
|
7834
|
+
}
|
|
7835
|
+
function pcm16ToFloat32(bytes) {
|
|
7836
|
+
const view = new DataView(bytes);
|
|
7837
|
+
const out = new Float32Array(Math.floor(bytes.byteLength / 2));
|
|
7838
|
+
for (let i = 0; i < out.length; i += 1) out[i] = view.getInt16(i * 2, true) / 32768;
|
|
7839
|
+
return out;
|
|
7840
|
+
}
|
|
7841
|
+
|
|
7842
|
+
// src/voice/persona.ts
|
|
7843
|
+
var VOICE_NAMES = [
|
|
7844
|
+
"alloy",
|
|
7845
|
+
"ash",
|
|
7846
|
+
"ballad",
|
|
7847
|
+
"cedar",
|
|
7848
|
+
"coral",
|
|
7849
|
+
"echo",
|
|
7850
|
+
"marin",
|
|
7851
|
+
"sage",
|
|
7852
|
+
"shimmer",
|
|
7853
|
+
"verse"
|
|
7854
|
+
];
|
|
7855
|
+
var VOICE_LANGUAGES = ["en", "es", "pt", "fr", "de", "it", "nl", "ar", "hi", "ja", "ko", "zh"];
|
|
7856
|
+
function encodePersona(persona) {
|
|
7857
|
+
if (!persona) return null;
|
|
7858
|
+
const fields = Object.fromEntries(Object.entries(persona).filter(([, v]) => v !== void 0));
|
|
7859
|
+
return Object.keys(fields).length ? JSON.stringify(fields) : null;
|
|
7860
|
+
}
|
|
7861
|
+
|
|
7862
|
+
// src/voice/screenAwake.ts
|
|
7863
|
+
function browserWakeLock() {
|
|
7864
|
+
return typeof navigator === "undefined" ? void 0 : navigator.wakeLock;
|
|
7865
|
+
}
|
|
7866
|
+
var ScreenAwake = class {
|
|
7867
|
+
constructor(api = browserWakeLock()) {
|
|
7868
|
+
this.api = api;
|
|
7869
|
+
this.sentinel = null;
|
|
7870
|
+
this.wanted = false;
|
|
7871
|
+
this.requesting = false;
|
|
7872
|
+
this.onVisibility = () => {
|
|
7873
|
+
if (document.visibilityState === "visible") void this.take();
|
|
7874
|
+
};
|
|
7875
|
+
}
|
|
7876
|
+
/** The screen is being kept on right now. */
|
|
7877
|
+
get held() {
|
|
7878
|
+
return !!this.sentinel && !this.sentinel.released;
|
|
7879
|
+
}
|
|
7880
|
+
/** Keep the screen on until `release`. Call it from the tap that starts
|
|
7881
|
+
* the session. */
|
|
7882
|
+
hold() {
|
|
7883
|
+
if (!this.api || this.wanted) return;
|
|
7884
|
+
this.wanted = true;
|
|
7885
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
7886
|
+
void this.take();
|
|
7887
|
+
}
|
|
7888
|
+
release() {
|
|
7889
|
+
if (!this.wanted) return;
|
|
7890
|
+
this.wanted = false;
|
|
7891
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
7892
|
+
const sentinel = this.sentinel;
|
|
7893
|
+
this.sentinel = null;
|
|
7894
|
+
void sentinel?.release().catch(() => void 0);
|
|
7895
|
+
}
|
|
7896
|
+
async take() {
|
|
7897
|
+
if (!this.api || !this.wanted || this.held || this.requesting || document.visibilityState !== "visible") return;
|
|
7898
|
+
this.requesting = true;
|
|
7899
|
+
try {
|
|
7900
|
+
const sentinel = await this.api.request("screen");
|
|
7901
|
+
if (this.wanted) this.sentinel = sentinel;
|
|
7902
|
+
else void sentinel.release().catch(() => void 0);
|
|
7903
|
+
} catch {
|
|
7904
|
+
} finally {
|
|
7905
|
+
this.requesting = false;
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
};
|
|
7909
|
+
|
|
7910
|
+
// src/voice/worklet.ts
|
|
7911
|
+
var VOICE_CAPTURE_WORKLET_SOURCE = `class YfVoiceCapture extends AudioWorkletProcessor {
|
|
7912
|
+
constructor() {
|
|
7913
|
+
super();
|
|
7914
|
+
this.batch = new Float32Array(2048);
|
|
7915
|
+
this.filled = 0;
|
|
7916
|
+
}
|
|
7917
|
+
process(inputs) {
|
|
7918
|
+
const channel = inputs[0] && inputs[0][0];
|
|
7919
|
+
if (channel) {
|
|
7920
|
+
let offset = 0;
|
|
7921
|
+
while (offset < channel.length) {
|
|
7922
|
+
const n = Math.min(channel.length - offset, this.batch.length - this.filled);
|
|
7923
|
+
this.batch.set(channel.subarray(offset, offset + n), this.filled);
|
|
7924
|
+
this.filled += n;
|
|
7925
|
+
offset += n;
|
|
7926
|
+
if (this.filled === this.batch.length) {
|
|
7927
|
+
this.port.postMessage(this.batch, [this.batch.buffer]);
|
|
7928
|
+
this.batch = new Float32Array(2048);
|
|
7929
|
+
this.filled = 0;
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
return true;
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7936
|
+
registerProcessor('yf-voice-capture', YfVoiceCapture);
|
|
7937
|
+
`;
|
|
7938
|
+
var VOICE_CAPTURE_PROCESSOR = "yf-voice-capture";
|
|
7939
|
+
|
|
7940
|
+
// src/voice/session.ts
|
|
7941
|
+
function socketBase(base) {
|
|
7942
|
+
return base.replace(/^http/i, "ws").replace(/\/+$/, "");
|
|
7943
|
+
}
|
|
7944
|
+
var OFF = { enabled: false, blocked: null, maxSessionSecs: 0, persona: null };
|
|
7945
|
+
async function getVoiceStatus(connection, signal) {
|
|
7946
|
+
const token = await connection.getToken();
|
|
7947
|
+
if (!token) return OFF;
|
|
7948
|
+
try {
|
|
7949
|
+
const res = await fetch(`${connection.baseUrl.replace(/\/+$/, "")}/voice/status`, {
|
|
7950
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
7951
|
+
signal
|
|
7952
|
+
});
|
|
7953
|
+
if (!res.ok) return OFF;
|
|
7954
|
+
const body = await res.json();
|
|
7955
|
+
return {
|
|
7956
|
+
enabled: body?.enabled === true,
|
|
7957
|
+
blocked: typeof body?.blocked === "string" ? body.blocked : null,
|
|
7958
|
+
maxSessionSecs: Number(body?.max_session_secs) || 0,
|
|
7959
|
+
persona: body?.persona && typeof body.persona.name === "string" ? body.persona : null
|
|
7960
|
+
};
|
|
7961
|
+
} catch {
|
|
7962
|
+
return OFF;
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
var SILENT = { input: 0, output: 0 };
|
|
7966
|
+
function rms(analyser, buf) {
|
|
7967
|
+
if (!analyser || !buf) return 0;
|
|
7968
|
+
analyser.getFloatTimeDomainData(buf);
|
|
7969
|
+
let sum = 0;
|
|
7970
|
+
for (let i = 0; i < buf.length; i += 1) sum += buf[i] * buf[i];
|
|
7971
|
+
return Math.sqrt(sum / buf.length);
|
|
7972
|
+
}
|
|
7973
|
+
var PcmPlayer = class {
|
|
7974
|
+
constructor(ctx, out, onIdle) {
|
|
7975
|
+
this.ctx = ctx;
|
|
7976
|
+
this.out = out;
|
|
7977
|
+
this.onIdle = onIdle;
|
|
7978
|
+
this.next = 0;
|
|
7979
|
+
this.live = /* @__PURE__ */ new Set();
|
|
7980
|
+
}
|
|
7981
|
+
get playing() {
|
|
7982
|
+
return this.live.size > 0;
|
|
7983
|
+
}
|
|
7984
|
+
enqueue(bytes) {
|
|
7985
|
+
const samples = pcm16ToFloat32(bytes);
|
|
7986
|
+
if (samples.length === 0) return;
|
|
7987
|
+
const buffer = this.ctx.createBuffer(1, samples.length, WIRE_RATE);
|
|
7988
|
+
buffer.getChannelData(0).set(samples);
|
|
7989
|
+
const source = this.ctx.createBufferSource();
|
|
7990
|
+
source.buffer = buffer;
|
|
7991
|
+
source.connect(this.out);
|
|
7992
|
+
const at = Math.max(this.ctx.currentTime + 0.02, this.next);
|
|
7993
|
+
source.start(at);
|
|
7994
|
+
this.next = at + buffer.duration;
|
|
7995
|
+
this.live.add(source);
|
|
7996
|
+
source.onended = () => {
|
|
7997
|
+
this.live.delete(source);
|
|
7998
|
+
if (this.live.size === 0) this.onIdle();
|
|
7999
|
+
};
|
|
8000
|
+
}
|
|
8001
|
+
stop() {
|
|
8002
|
+
this.live.forEach((source) => {
|
|
8003
|
+
source.onended = null;
|
|
8004
|
+
try {
|
|
8005
|
+
source.stop();
|
|
8006
|
+
} catch {
|
|
8007
|
+
}
|
|
8008
|
+
});
|
|
8009
|
+
this.live.clear();
|
|
8010
|
+
this.next = 0;
|
|
8011
|
+
}
|
|
8012
|
+
};
|
|
8013
|
+
var FINISH_GRACE_MS = 800;
|
|
8014
|
+
var FINISH_MAX_WAIT_MS = 15e3;
|
|
8015
|
+
var CLOSE_REASONS = {
|
|
8016
|
+
time_limit: "time_limit",
|
|
8017
|
+
replaced: "replaced",
|
|
8018
|
+
upstream_closed: "lost",
|
|
8019
|
+
upstream_error: "lost"
|
|
8020
|
+
};
|
|
8021
|
+
function voiceSocketUrl(connection, token) {
|
|
8022
|
+
const params = new URLSearchParams({ access_token: token });
|
|
8023
|
+
if (connection.threadId) params.set("thread_id", connection.threadId);
|
|
8024
|
+
if (connection.workingGroupId) params.set("working_group_id", connection.workingGroupId);
|
|
8025
|
+
const persona = encodePersona(connection.persona);
|
|
8026
|
+
if (persona) params.set("persona", persona);
|
|
8027
|
+
return `${socketBase(connection.baseUrl)}/voice/realtime?${params.toString()}`;
|
|
8028
|
+
}
|
|
8029
|
+
var VoiceSession = class {
|
|
8030
|
+
constructor(h, connection) {
|
|
8031
|
+
this.h = h;
|
|
8032
|
+
this.connection = connection;
|
|
8033
|
+
this.ctx = null;
|
|
8034
|
+
this.stream = null;
|
|
8035
|
+
this.node = null;
|
|
8036
|
+
this.ws = null;
|
|
8037
|
+
this.player = null;
|
|
8038
|
+
this.inputAnalyser = null;
|
|
8039
|
+
this.outputAnalyser = null;
|
|
8040
|
+
this.inputBuf = null;
|
|
8041
|
+
this.outputBuf = null;
|
|
8042
|
+
this.ready = false;
|
|
8043
|
+
this.ended = false;
|
|
8044
|
+
this.phase = null;
|
|
8045
|
+
/** Keeps a phone from locking mid-conversation (see `screenAwake.ts`). */
|
|
8046
|
+
this.awake = new ScreenAwake();
|
|
8047
|
+
/** The voice's questions to the host, one at a time (see `askQueue.ts`). */
|
|
8048
|
+
this.asks = new AskQueue(
|
|
8049
|
+
(ask, signal) => {
|
|
8050
|
+
let open = true;
|
|
8051
|
+
const progress = (line, kind) => {
|
|
8052
|
+
if (open && !signal.aborted && line.trim()) this.send({ type: "progress", kind, text: line.slice(0, 240) });
|
|
8053
|
+
};
|
|
8054
|
+
return this.h.onAsk(ask.question, ask.heard, ask.utterance, progress, signal).finally(() => {
|
|
8055
|
+
open = false;
|
|
8056
|
+
});
|
|
8057
|
+
},
|
|
8058
|
+
(ask, output, speak) => {
|
|
8059
|
+
if (this.ended) return;
|
|
8060
|
+
this.send({ type: "tool_result", call_id: ask.callId, output, ...speak ? {} : { speak: false } });
|
|
8061
|
+
this.settle();
|
|
8062
|
+
}
|
|
8063
|
+
);
|
|
8064
|
+
this.heard = null;
|
|
8065
|
+
this.partialHeard = "";
|
|
8066
|
+
this.saying = "";
|
|
8067
|
+
this.endingReason = null;
|
|
8068
|
+
this.finishTimer = null;
|
|
8069
|
+
this.muted = false;
|
|
8070
|
+
this.blobUrl = null;
|
|
8071
|
+
}
|
|
8072
|
+
/** Call from the tap itself: iOS only lets audio start inside a gesture,
|
|
8073
|
+
* so the AudioContext is created before the first `await`. */
|
|
8074
|
+
async start() {
|
|
8075
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
8076
|
+
if (!Ctx || !navigator.mediaDevices?.getUserMedia || typeof AudioWorkletNode === "undefined") {
|
|
8077
|
+
this.end("unavailable", "This browser cannot do live voice.");
|
|
8078
|
+
return;
|
|
8079
|
+
}
|
|
8080
|
+
const ctx = new Ctx();
|
|
8081
|
+
this.ctx = ctx;
|
|
8082
|
+
void ctx.resume();
|
|
8083
|
+
this.awake.hold();
|
|
8084
|
+
this.setPhase("connecting");
|
|
8085
|
+
const status = await getVoiceStatus(this.connection);
|
|
8086
|
+
if (this.ended) return;
|
|
8087
|
+
if (status.blocked) {
|
|
8088
|
+
this.end("blocked", status.blocked);
|
|
8089
|
+
return;
|
|
8090
|
+
}
|
|
8091
|
+
if (!status.enabled) {
|
|
8092
|
+
this.end("unavailable", "Voice is not switched on.");
|
|
8093
|
+
return;
|
|
8094
|
+
}
|
|
8095
|
+
try {
|
|
8096
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
8097
|
+
audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true }
|
|
8098
|
+
});
|
|
8099
|
+
this.applyMute();
|
|
8100
|
+
} catch (e) {
|
|
8101
|
+
this.end("mic_denied", e?.message);
|
|
8102
|
+
return;
|
|
8103
|
+
}
|
|
8104
|
+
if (this.ended) {
|
|
8105
|
+
this.release();
|
|
8106
|
+
return;
|
|
8107
|
+
}
|
|
8108
|
+
try {
|
|
8109
|
+
let url = this.connection.workletUrl;
|
|
8110
|
+
if (!url) {
|
|
8111
|
+
this.blobUrl = URL.createObjectURL(new Blob([VOICE_CAPTURE_WORKLET_SOURCE], { type: "application/javascript" }));
|
|
8112
|
+
url = this.blobUrl;
|
|
8113
|
+
}
|
|
8114
|
+
await ctx.audioWorklet.addModule(url);
|
|
8115
|
+
} catch (e) {
|
|
8116
|
+
this.end("unavailable", e?.message);
|
|
8117
|
+
return;
|
|
8118
|
+
}
|
|
8119
|
+
if (this.ended) return;
|
|
8120
|
+
const down = new Downsampler(ctx.sampleRate);
|
|
8121
|
+
const frames = new FrameBuffer();
|
|
8122
|
+
const node = new AudioWorkletNode(ctx, VOICE_CAPTURE_PROCESSOR);
|
|
8123
|
+
this.node = node;
|
|
8124
|
+
node.port.onmessage = (e) => {
|
|
8125
|
+
const ws2 = this.ws;
|
|
8126
|
+
if (!this.ready || !ws2 || ws2.readyState !== WebSocket.OPEN) return;
|
|
8127
|
+
for (const frame of frames.push(down.push(e.data))) ws2.send(frame);
|
|
8128
|
+
};
|
|
8129
|
+
const mic = ctx.createMediaStreamSource(this.stream);
|
|
8130
|
+
const inputAnalyser = ctx.createAnalyser();
|
|
8131
|
+
inputAnalyser.fftSize = 1024;
|
|
8132
|
+
const outputAnalyser = ctx.createAnalyser();
|
|
8133
|
+
outputAnalyser.fftSize = 1024;
|
|
8134
|
+
this.inputAnalyser = inputAnalyser;
|
|
8135
|
+
this.outputAnalyser = outputAnalyser;
|
|
8136
|
+
this.inputBuf = new Float32Array(inputAnalyser.fftSize);
|
|
8137
|
+
this.outputBuf = new Float32Array(outputAnalyser.fftSize);
|
|
8138
|
+
const mute = ctx.createGain();
|
|
8139
|
+
mute.gain.value = 0;
|
|
8140
|
+
mic.connect(inputAnalyser);
|
|
8141
|
+
inputAnalyser.connect(node);
|
|
8142
|
+
node.connect(mute);
|
|
8143
|
+
mute.connect(ctx.destination);
|
|
8144
|
+
outputAnalyser.connect(ctx.destination);
|
|
8145
|
+
this.player = new PcmPlayer(ctx, outputAnalyser, () => this.settle());
|
|
8146
|
+
const token = await this.connection.getToken();
|
|
8147
|
+
if (this.ended) return;
|
|
8148
|
+
if (!token) {
|
|
8149
|
+
this.end("unavailable", "Sign in again to use voice.");
|
|
8150
|
+
return;
|
|
8151
|
+
}
|
|
8152
|
+
const ws = new WebSocket(voiceSocketUrl(this.connection, token));
|
|
8153
|
+
ws.binaryType = "arraybuffer";
|
|
8154
|
+
this.ws = ws;
|
|
8155
|
+
ws.onmessage = (e) => {
|
|
8156
|
+
if (typeof e.data === "string") this.onEvent(e.data);
|
|
8157
|
+
else this.onAudio(e.data);
|
|
8158
|
+
};
|
|
8159
|
+
ws.onclose = (e) => {
|
|
8160
|
+
if (this.ended) return;
|
|
8161
|
+
if (!this.ready) {
|
|
8162
|
+
this.end("unavailable");
|
|
8163
|
+
return;
|
|
8164
|
+
}
|
|
8165
|
+
this.end(this.endingReason ?? CLOSE_REASONS[e.reason] ?? "lost");
|
|
8166
|
+
};
|
|
8167
|
+
}
|
|
8168
|
+
stop() {
|
|
8169
|
+
this.end("stopped");
|
|
8170
|
+
}
|
|
8171
|
+
/**
|
|
8172
|
+
* Mute the microphone, or not. The track goes silent rather than the
|
|
8173
|
+
* stream stopping: the service keeps hearing (silence), so whatever the
|
|
8174
|
+
* person was saying ends as a turn and nothing hangs half-heard. The
|
|
8175
|
+
* voice can still talk while they are muted.
|
|
8176
|
+
*/
|
|
8177
|
+
setMuted(muted) {
|
|
8178
|
+
this.muted = muted;
|
|
8179
|
+
this.applyMute();
|
|
8180
|
+
}
|
|
8181
|
+
get isMuted() {
|
|
8182
|
+
return this.muted;
|
|
8183
|
+
}
|
|
8184
|
+
applyMute() {
|
|
8185
|
+
this.stream?.getAudioTracks().forEach((track) => {
|
|
8186
|
+
track.enabled = !this.muted;
|
|
8187
|
+
});
|
|
8188
|
+
}
|
|
8189
|
+
/** Cheap enough to call every animation frame. */
|
|
8190
|
+
levels() {
|
|
8191
|
+
if (this.ended) return SILENT;
|
|
8192
|
+
return {
|
|
8193
|
+
input: rms(this.inputAnalyser, this.inputBuf),
|
|
8194
|
+
output: this.player?.playing ? rms(this.outputAnalyser, this.outputBuf) : 0
|
|
8195
|
+
};
|
|
8196
|
+
}
|
|
8197
|
+
setPhase(phase) {
|
|
8198
|
+
if (this.ended || this.phase === phase) return;
|
|
8199
|
+
this.phase = phase;
|
|
8200
|
+
this.h.onPhase(phase);
|
|
8201
|
+
}
|
|
8202
|
+
settle() {
|
|
8203
|
+
if (this.asks.size > 0) this.setPhase("thinking");
|
|
8204
|
+
else if (this.player?.playing) this.setPhase("speaking");
|
|
8205
|
+
else this.setPhase("listening");
|
|
8206
|
+
}
|
|
8207
|
+
send(message) {
|
|
8208
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
|
|
8209
|
+
}
|
|
8210
|
+
onAudio(bytes) {
|
|
8211
|
+
if (this.ended || !this.player) return;
|
|
8212
|
+
this.player.enqueue(bytes);
|
|
8213
|
+
this.setPhase("speaking");
|
|
8214
|
+
}
|
|
8215
|
+
onEvent(raw) {
|
|
8216
|
+
let ev;
|
|
8217
|
+
try {
|
|
8218
|
+
ev = JSON.parse(raw);
|
|
8219
|
+
} catch {
|
|
8220
|
+
return;
|
|
8221
|
+
}
|
|
8222
|
+
const text = (k) => typeof ev[k] === "string" ? ev[k] : "";
|
|
8223
|
+
switch (ev.type) {
|
|
8224
|
+
case "session.ready":
|
|
8225
|
+
this.ready = true;
|
|
8226
|
+
this.setPhase("listening");
|
|
8227
|
+
break;
|
|
8228
|
+
case "input_audio_buffer.speech_started":
|
|
8229
|
+
this.cancelFinish();
|
|
8230
|
+
this.player?.stop();
|
|
8231
|
+
this.saying = "";
|
|
8232
|
+
this.partialHeard = "";
|
|
8233
|
+
this.heard = null;
|
|
8234
|
+
this.setPhase("hearing");
|
|
8235
|
+
break;
|
|
8236
|
+
case "input_audio_buffer.speech_stopped":
|
|
8237
|
+
this.setPhase("thinking");
|
|
8238
|
+
break;
|
|
8239
|
+
case "conversation.item.input_audio_transcription.delta":
|
|
8240
|
+
this.partialHeard += text("delta");
|
|
8241
|
+
this.h.onHeard(this.partialHeard, false);
|
|
8242
|
+
break;
|
|
8243
|
+
case "conversation.item.input_audio_transcription.completed":
|
|
8244
|
+
this.heard = text("transcript").trim() || null;
|
|
8245
|
+
this.partialHeard = "";
|
|
8246
|
+
this.h.onHeard(this.heard ?? "", true);
|
|
8247
|
+
if (this.heard && text("message_id")) {
|
|
8248
|
+
this.h.onUtterance?.({ itemId: text("item_id"), messageId: text("message_id"), text: this.heard });
|
|
8249
|
+
}
|
|
8250
|
+
break;
|
|
8251
|
+
case "voice.reply":
|
|
8252
|
+
if (text("message_id") && text("text")) {
|
|
8253
|
+
this.h.onReply?.({
|
|
8254
|
+
messageId: text("message_id"),
|
|
8255
|
+
itemId: text("item_id") || null,
|
|
8256
|
+
text: text("text"),
|
|
8257
|
+
interrupted: ev.interrupted === true
|
|
8258
|
+
});
|
|
8259
|
+
}
|
|
8260
|
+
break;
|
|
8261
|
+
case "response.audio_transcript.delta":
|
|
8262
|
+
this.saying += text("delta");
|
|
8263
|
+
this.h.onSaying(this.saying, false);
|
|
8264
|
+
break;
|
|
8265
|
+
case "response.audio_transcript.done":
|
|
8266
|
+
this.h.onSaying(text("transcript") || this.saying, true);
|
|
8267
|
+
this.saying = "";
|
|
8268
|
+
break;
|
|
8269
|
+
case "tool_call":
|
|
8270
|
+
this.asks.add({
|
|
8271
|
+
callId: text("call_id"),
|
|
8272
|
+
question: text("question"),
|
|
8273
|
+
replaces: ev.replaces_earlier === true,
|
|
8274
|
+
heard: this.heard,
|
|
8275
|
+
utterance: text("message_id") ? { itemId: text("item_id"), messageId: text("message_id") } : null
|
|
8276
|
+
});
|
|
8277
|
+
this.setPhase("thinking");
|
|
8278
|
+
break;
|
|
8279
|
+
case "response.done":
|
|
8280
|
+
if (!this.player?.playing) this.settle();
|
|
8281
|
+
break;
|
|
8282
|
+
case "session.ending":
|
|
8283
|
+
this.endingReason = CLOSE_REASONS[text("reason")] ?? "lost";
|
|
8284
|
+
break;
|
|
8285
|
+
case "session.finish":
|
|
8286
|
+
this.finishAfterGoodbye();
|
|
8287
|
+
break;
|
|
8288
|
+
case "error":
|
|
8289
|
+
console.warn("[voice]", ev.code, ev.message);
|
|
8290
|
+
break;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
/** They are done: end once the goodbye has finished playing. */
|
|
8294
|
+
finishAfterGoodbye() {
|
|
8295
|
+
this.cancelFinish();
|
|
8296
|
+
const deadline = Date.now() + FINISH_MAX_WAIT_MS;
|
|
8297
|
+
const check = () => {
|
|
8298
|
+
if (this.ended) return;
|
|
8299
|
+
if (this.player?.playing && Date.now() < deadline) {
|
|
8300
|
+
this.finishTimer = setTimeout(check, 200);
|
|
8301
|
+
return;
|
|
8302
|
+
}
|
|
8303
|
+
this.finishTimer = setTimeout(() => this.end("finished"), FINISH_GRACE_MS);
|
|
8304
|
+
};
|
|
8305
|
+
check();
|
|
8306
|
+
}
|
|
8307
|
+
cancelFinish() {
|
|
8308
|
+
if (this.finishTimer) clearTimeout(this.finishTimer);
|
|
8309
|
+
this.finishTimer = null;
|
|
8310
|
+
}
|
|
8311
|
+
release() {
|
|
8312
|
+
this.awake.release();
|
|
8313
|
+
this.node?.port.close();
|
|
8314
|
+
this.node?.disconnect();
|
|
8315
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
8316
|
+
this.player?.stop();
|
|
8317
|
+
void this.ctx?.close().catch(() => void 0);
|
|
8318
|
+
if (this.blobUrl) URL.revokeObjectURL(this.blobUrl);
|
|
8319
|
+
this.blobUrl = null;
|
|
8320
|
+
this.inputAnalyser = null;
|
|
8321
|
+
this.outputAnalyser = null;
|
|
8322
|
+
this.node = null;
|
|
8323
|
+
this.stream = null;
|
|
8324
|
+
this.ctx = null;
|
|
8325
|
+
}
|
|
8326
|
+
end(reason, detail) {
|
|
8327
|
+
if (this.ended) return;
|
|
8328
|
+
this.ended = true;
|
|
8329
|
+
this.cancelFinish();
|
|
8330
|
+
const ws = this.ws;
|
|
8331
|
+
this.ws = null;
|
|
8332
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) ws.close(1e3);
|
|
8333
|
+
this.release();
|
|
8334
|
+
this.h.onEnd(reason, detail);
|
|
8335
|
+
}
|
|
8336
|
+
};
|
|
8337
|
+
|
|
8338
|
+
// src/voice/voiceprint.ts
|
|
8339
|
+
var GLYPHS = "\uFF71\uFF72\uFF73\uFF74\uFF75\uFF76\uFF77\uFF78\uFF79\uFF7A\uFF7B\uFF7C\uFF7D\uFF7E\uFF7F\uFF80\uFF81\uFF82\uFF83\uFF84\uFF85\uFF86\uFF87\uFF88\uFF89\uFF8A\uFF8B\uFF8C\uFF8D\uFF8E\uFF8F\uFF90\uFF91\uFF92\uFF93\uFF94\uFF95\uFF96\uFF97\uFF98\uFF99\uFF9A\uFF9B\uFF9C\uFF9D01".split("");
|
|
8340
|
+
var STEP_MS = 42;
|
|
8341
|
+
function loudness(rms2) {
|
|
8342
|
+
const v = Math.max(0, rms2 - 4e-3);
|
|
8343
|
+
return Math.min(1, Math.sqrt(v * 7));
|
|
8344
|
+
}
|
|
8345
|
+
function nextColumn(levels, phase, t, seed) {
|
|
8346
|
+
const you = loudness(levels.input);
|
|
8347
|
+
const javier = loudness(levels.output);
|
|
8348
|
+
if (javier > 0.05 && javier >= you * 0.8) return { amp: javier, voice: 2 /* Javier */, seed };
|
|
8349
|
+
if (you > 0.12) return { amp: you, voice: 1 /* You */, seed };
|
|
8350
|
+
if (phase === "thinking" || phase === "connecting") {
|
|
8351
|
+
const swell = 0.24 + 0.17 * Math.sin(t / 170) * Math.sin(t / 530);
|
|
8352
|
+
return { amp: Math.max(0.06, swell), voice: 3 /* Working */, seed };
|
|
8353
|
+
}
|
|
8354
|
+
return { amp: 0.06 + 0.035 * Math.sin(t / 650), voice: 0 /* Quiet */, seed };
|
|
8355
|
+
}
|
|
8356
|
+
function cellIntensity(amp, rows, row) {
|
|
8357
|
+
const half = rows / 2;
|
|
8358
|
+
const d = Math.abs(row + 0.5 - half) / half;
|
|
8359
|
+
const reach = Math.max(amp, 0);
|
|
8360
|
+
if (reach <= 0 || d > reach) return 0;
|
|
8361
|
+
return 0.5 + 0.5 * Math.pow(1 - d / reach, 0.8);
|
|
8362
|
+
}
|
|
8363
|
+
function hash(a, b) {
|
|
8364
|
+
let h = Math.imul(a | 0, 374761393) + Math.imul(b | 0, 668265263) | 0;
|
|
8365
|
+
h = Math.imul(h ^ h >>> 13, 1274126177);
|
|
8366
|
+
return (h ^ h >>> 16) >>> 0;
|
|
8367
|
+
}
|
|
8368
|
+
function glyphFor(seed, row) {
|
|
8369
|
+
return GLYPHS[hash(seed, row) % GLYPHS.length];
|
|
8370
|
+
}
|
|
8371
|
+
function ageFade(x, width) {
|
|
8372
|
+
const span = width * 0.42;
|
|
8373
|
+
if (span <= 0) return 1;
|
|
8374
|
+
const k = Math.min(1, Math.max(0, x / span));
|
|
8375
|
+
return k * k * (3 - 2 * k);
|
|
8376
|
+
}
|
|
8377
|
+
function clock(ms) {
|
|
8378
|
+
const s = Math.max(0, Math.floor(ms / 1e3));
|
|
8379
|
+
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
8380
|
+
}
|
|
8381
|
+
function captionTail(text, max) {
|
|
8382
|
+
const t = text.trim();
|
|
8383
|
+
if (t.length <= max) return t;
|
|
8384
|
+
const cut = t.slice(t.length - max);
|
|
8385
|
+
const space = cut.indexOf(" ");
|
|
8386
|
+
return `\u2026${space > 0 && space < 24 ? cut.slice(space + 1) : cut}`;
|
|
8387
|
+
}
|
|
8388
|
+
function parseCssColor(value, fallback) {
|
|
8389
|
+
const v = value.trim();
|
|
8390
|
+
const srgb = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/i.exec(v);
|
|
8391
|
+
if (srgb) return [srgb[1], srgb[2], srgb[3]].map((n) => Math.round(Math.min(1, Math.max(0, Number(n))) * 255));
|
|
8392
|
+
const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(v);
|
|
8393
|
+
if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])].map((n) => Math.round(n));
|
|
8394
|
+
return fallback;
|
|
8395
|
+
}
|
|
8396
|
+
|
|
8397
|
+
// src/voice/useVoiceSession.ts
|
|
8398
|
+
function useVoiceSession(host, connection) {
|
|
8399
|
+
const [available, setAvailable] = React15.useState(false);
|
|
8400
|
+
const [defaultName, setDefaultName] = React15.useState(null);
|
|
8401
|
+
const [maxSessionSecs, setMaxSessionSecs] = React15.useState(0);
|
|
8402
|
+
const [phase, setPhase] = React15.useState("off");
|
|
8403
|
+
const [heard, setHeard] = React15.useState("");
|
|
8404
|
+
const [saying, setSaying] = React15.useState("");
|
|
8405
|
+
const [ended, setEnded] = React15.useState(null);
|
|
8406
|
+
const [startedAt, setStartedAt] = React15.useState(null);
|
|
8407
|
+
const [muted, setMuted] = React15.useState(false);
|
|
8408
|
+
const session = React15.useRef(null);
|
|
8409
|
+
const hostRef = React15.useRef(host);
|
|
8410
|
+
hostRef.current = host;
|
|
8411
|
+
const connectionRef = React15.useRef(connection);
|
|
8412
|
+
connectionRef.current = connection;
|
|
8413
|
+
const { baseUrl } = connection;
|
|
8414
|
+
React15.useEffect(() => {
|
|
8415
|
+
const controller = new AbortController();
|
|
8416
|
+
void getVoiceStatus(connectionRef.current, controller.signal).then((s) => {
|
|
8417
|
+
if (controller.signal.aborted) return;
|
|
8418
|
+
setAvailable(s.enabled);
|
|
8419
|
+
setMaxSessionSecs(s.maxSessionSecs);
|
|
8420
|
+
setDefaultName(s.persona?.name ?? null);
|
|
8421
|
+
});
|
|
8422
|
+
return () => controller.abort();
|
|
8423
|
+
}, [baseUrl]);
|
|
8424
|
+
const start = React15.useCallback(() => {
|
|
8425
|
+
if (session.current) return;
|
|
8426
|
+
setEnded(null);
|
|
8427
|
+
setHeard("");
|
|
8428
|
+
setSaying("");
|
|
8429
|
+
setStartedAt(null);
|
|
8430
|
+
setMuted(false);
|
|
8431
|
+
const s = new VoiceSession(
|
|
8432
|
+
{
|
|
8433
|
+
onPhase: (p) => {
|
|
8434
|
+
setPhase(p);
|
|
8435
|
+
if (p !== "connecting") setStartedAt((at) => at ?? Date.now());
|
|
8436
|
+
},
|
|
8437
|
+
onHeard: (text) => setHeard(text),
|
|
8438
|
+
onSaying: (text) => setSaying(text),
|
|
8439
|
+
onAsk: (question, h, utterance, progress, signal) => hostRef.current.onAsk(question, h, utterance, progress, signal),
|
|
8440
|
+
onUtterance: (u) => hostRef.current.onUtterance?.(u),
|
|
8441
|
+
onReply: (r) => hostRef.current.onReply?.(r),
|
|
8442
|
+
onEnd: (reason, detail) => {
|
|
8443
|
+
if (session.current === s) session.current = null;
|
|
8444
|
+
setMuted(false);
|
|
8445
|
+
setPhase("off");
|
|
8446
|
+
setStartedAt(null);
|
|
8447
|
+
setEnded(reason === "stopped" || reason === "finished" ? null : { reason, detail });
|
|
8448
|
+
}
|
|
8449
|
+
},
|
|
8450
|
+
{ ...connectionRef.current }
|
|
8451
|
+
);
|
|
8452
|
+
session.current = s;
|
|
8453
|
+
void s.start();
|
|
8454
|
+
}, []);
|
|
8455
|
+
const stop = React15.useCallback(() => session.current?.stop(), []);
|
|
8456
|
+
const toggle = React15.useCallback(() => session.current ? stop() : start(), [start, stop]);
|
|
8457
|
+
const dismiss = React15.useCallback(() => setEnded(null), []);
|
|
8458
|
+
const toggleMute = React15.useCallback(() => {
|
|
8459
|
+
const s = session.current;
|
|
8460
|
+
if (!s) return;
|
|
8461
|
+
s.setMuted(!s.isMuted);
|
|
8462
|
+
setMuted(s.isMuted);
|
|
8463
|
+
}, []);
|
|
8464
|
+
const levels = React15.useCallback(() => session.current?.levels() ?? SILENT, []);
|
|
8465
|
+
const level = React15.useCallback(() => {
|
|
8466
|
+
const l = levels();
|
|
8467
|
+
return Math.max(loudness(l.input), loudness(l.output));
|
|
8468
|
+
}, [levels]);
|
|
8469
|
+
React15.useEffect(() => () => session.current?.stop(), []);
|
|
8470
|
+
return {
|
|
8471
|
+
available,
|
|
8472
|
+
defaultName,
|
|
8473
|
+
phase,
|
|
8474
|
+
heard,
|
|
8475
|
+
saying,
|
|
8476
|
+
ended,
|
|
8477
|
+
startedAt,
|
|
8478
|
+
maxSessionSecs,
|
|
8479
|
+
levels,
|
|
8480
|
+
level,
|
|
8481
|
+
buttonState: phase === "off" ? "off" : phase === "connecting" ? "connecting" : "live",
|
|
8482
|
+
muted,
|
|
8483
|
+
toggleMute,
|
|
8484
|
+
start,
|
|
8485
|
+
stop,
|
|
8486
|
+
toggle,
|
|
8487
|
+
dismiss
|
|
8488
|
+
};
|
|
8489
|
+
}
|
|
8490
|
+
function voiceEndedMessage(ended, name = "the assistant") {
|
|
8491
|
+
if (!ended) return null;
|
|
8492
|
+
switch (ended.reason) {
|
|
8493
|
+
case "time_limit":
|
|
8494
|
+
return "Voice ends after a while to keep costs in check. Tap the microphone to keep talking.";
|
|
8495
|
+
case "replaced":
|
|
8496
|
+
return "Voice moved to your other window.";
|
|
8497
|
+
case "blocked":
|
|
8498
|
+
return ended.detail === "card_required" ? "Add a card to use voice." : ended.detail === "credit_exhausted" ? "You are out of credit. Top up to use voice." : "Voice needs an active billing plan.";
|
|
8499
|
+
case "mic_denied":
|
|
8500
|
+
return `Allow microphone access to talk to ${name}.`;
|
|
8501
|
+
case "unavailable":
|
|
8502
|
+
return ended.detail && !/^[a-z_]+$/.test(ended.detail) ? ended.detail : "Voice could not connect. Try again in a moment.";
|
|
8503
|
+
case "lost":
|
|
8504
|
+
return "Voice disconnected. Tap the microphone to reconnect.";
|
|
8505
|
+
default:
|
|
8506
|
+
return null;
|
|
8507
|
+
}
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8510
|
+
// src/voice/styles.ts
|
|
8511
|
+
var VOICE_DECK_CSS = `
|
|
8512
|
+
@property --yf-voice-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
|
|
8513
|
+
@keyframes yf-voice-orbit { to { --yf-voice-angle: 360deg; } }
|
|
8514
|
+
@keyframes yf-voice-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
|
|
8515
|
+
@keyframes yf-voice-beacon {
|
|
8516
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--yf-voice-live) 55%, transparent); }
|
|
8517
|
+
70% { box-shadow: 0 0 0 9px color-mix(in srgb, var(--yf-voice-live) 0%, transparent); }
|
|
8518
|
+
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--yf-voice-live) 0%, transparent); }
|
|
8519
|
+
}
|
|
8520
|
+
.yf-voice-deck {
|
|
8521
|
+
--yf-voice-live: var(--yf-voice-accent);
|
|
8522
|
+
position: relative;
|
|
8523
|
+
border-radius: 16px;
|
|
8524
|
+
padding: 1px;
|
|
8525
|
+
background:
|
|
8526
|
+
conic-gradient(
|
|
8527
|
+
from var(--yf-voice-angle),
|
|
8528
|
+
transparent 0deg,
|
|
8529
|
+
color-mix(in srgb, var(--yf-voice-live) 95%, transparent) 50deg,
|
|
8530
|
+
color-mix(in srgb, var(--yf-voice-live) 15%, transparent) 110deg,
|
|
8531
|
+
transparent 180deg,
|
|
8532
|
+
transparent 360deg
|
|
8533
|
+
),
|
|
8534
|
+
linear-gradient(color-mix(in srgb, var(--yf-voice-live) 22%, transparent), color-mix(in srgb, var(--yf-voice-live) 22%, transparent));
|
|
8535
|
+
box-shadow: 0 24px 60px -32px color-mix(in srgb, var(--yf-voice-live) 65%, transparent);
|
|
8536
|
+
animation: yf-voice-orbit 5s linear infinite;
|
|
8537
|
+
transition: box-shadow 0.5s ease;
|
|
8538
|
+
}
|
|
8539
|
+
.yf-voice-deck[data-phase='hearing'] { --yf-voice-live: var(--yf-voice-ink); animation-duration: 2.6s; }
|
|
8540
|
+
.yf-voice-deck[data-phase='thinking'] { animation-duration: 1.4s; }
|
|
8541
|
+
.yf-voice-deck[data-phase='speaking'] { animation-duration: 3.2s; }
|
|
8542
|
+
.yf-voice-deck[data-phase='listening'],
|
|
8543
|
+
.yf-voice-deck[data-phase='connecting'] { animation-duration: 9s; }
|
|
8544
|
+
.yf-voice-deck__panel {
|
|
8545
|
+
position: relative;
|
|
8546
|
+
overflow: hidden;
|
|
8547
|
+
border-radius: 15px;
|
|
8548
|
+
background:
|
|
8549
|
+
radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--yf-voice-live) 13%, transparent), transparent 58%),
|
|
8550
|
+
radial-gradient(90% 120% at 0% 100%, color-mix(in srgb, var(--yf-voice-live) 5%, transparent), transparent 60%),
|
|
8551
|
+
color-mix(in srgb, var(--yf-voice-panel) 90%, transparent);
|
|
8552
|
+
-webkit-backdrop-filter: blur(14px) saturate(1.2);
|
|
8553
|
+
backdrop-filter: blur(14px) saturate(1.2);
|
|
8554
|
+
animation: yf-voice-rise 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
8555
|
+
}
|
|
8556
|
+
.yf-voice-beacon {
|
|
8557
|
+
width: 8px;
|
|
8558
|
+
height: 8px;
|
|
8559
|
+
flex-shrink: 0;
|
|
8560
|
+
border-radius: 999px;
|
|
8561
|
+
background: var(--yf-voice-live);
|
|
8562
|
+
animation: yf-voice-beacon 1.8s ease-out infinite;
|
|
8563
|
+
transition: background-color 0.3s ease;
|
|
8564
|
+
}
|
|
8565
|
+
.yf-voice-deck[data-phase='listening'] .yf-voice-beacon { animation-duration: 3.2s; }
|
|
8566
|
+
.yf-voice-phase {
|
|
8567
|
+
white-space: nowrap;
|
|
8568
|
+
font-size: 10.5px;
|
|
8569
|
+
font-weight: 600;
|
|
8570
|
+
letter-spacing: 0.14em;
|
|
8571
|
+
text-transform: uppercase;
|
|
8572
|
+
color: var(--yf-voice-live);
|
|
8573
|
+
transition: color 0.3s ease;
|
|
8574
|
+
}
|
|
8575
|
+
.yf-voice-clock { font-size: 11px; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--yf-voice-muted); }
|
|
8576
|
+
.yf-voice-clock[data-soon] { color: var(--yf-voice-accent); }
|
|
8577
|
+
.yf-voice-end {
|
|
8578
|
+
display: inline-flex;
|
|
8579
|
+
align-items: center;
|
|
8580
|
+
gap: 7px;
|
|
8581
|
+
height: 28px;
|
|
8582
|
+
padding: 0 12px 0 10px;
|
|
8583
|
+
border-radius: 999px;
|
|
8584
|
+
border: 1px solid color-mix(in srgb, var(--yf-voice-ink) 16%, transparent);
|
|
8585
|
+
background: color-mix(in srgb, var(--yf-voice-ink) 5%, transparent);
|
|
8586
|
+
color: color-mix(in srgb, var(--yf-voice-ink) 85%, transparent);
|
|
8587
|
+
font-size: 12px;
|
|
8588
|
+
font-weight: 500;
|
|
8589
|
+
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
8590
|
+
-webkit-tap-highlight-color: transparent;
|
|
8591
|
+
}
|
|
8592
|
+
.yf-voice-end:hover { background: color-mix(in srgb, var(--yf-voice-ink) 10%, transparent); border-color: color-mix(in srgb, var(--yf-voice-ink) 28%, transparent); color: var(--yf-voice-ink); }
|
|
8593
|
+
.yf-voice-end:active { transform: scale(0.97); }
|
|
8594
|
+
.yf-voice-end__mark { width: 8px; height: 8px; border-radius: 2px; background: currentColor; }
|
|
8595
|
+
.yf-voice-mute {
|
|
8596
|
+
display: inline-flex;
|
|
8597
|
+
align-items: center;
|
|
8598
|
+
justify-content: center;
|
|
8599
|
+
width: 28px;
|
|
8600
|
+
height: 28px;
|
|
8601
|
+
border-radius: 999px;
|
|
8602
|
+
border: 1px solid color-mix(in srgb, var(--yf-voice-ink) 16%, transparent);
|
|
8603
|
+
background: color-mix(in srgb, var(--yf-voice-ink) 5%, transparent);
|
|
8604
|
+
color: color-mix(in srgb, var(--yf-voice-ink) 85%, transparent);
|
|
8605
|
+
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
|
8606
|
+
-webkit-tap-highlight-color: transparent;
|
|
8607
|
+
}
|
|
8608
|
+
.yf-voice-mute svg { width: 14px; height: 14px; }
|
|
8609
|
+
.yf-voice-mute:hover { background: color-mix(in srgb, var(--yf-voice-ink) 10%, transparent); color: var(--yf-voice-ink); }
|
|
8610
|
+
.yf-voice-mute:active { transform: scale(0.95); }
|
|
8611
|
+
.yf-voice-mute:disabled { opacity: 0.4; }
|
|
8612
|
+
.yf-voice-mute[aria-pressed='true'] {
|
|
8613
|
+
border-color: color-mix(in srgb, var(--yf-voice-accent) 55%, transparent);
|
|
8614
|
+
background: color-mix(in srgb, var(--yf-voice-accent) 18%, transparent);
|
|
8615
|
+
color: var(--yf-voice-accent);
|
|
8616
|
+
}
|
|
8617
|
+
.yf-voice-deck[data-muted] .yf-voice-beacon { background: var(--yf-voice-muted); animation: none; }
|
|
8618
|
+
.yf-voice-deck[data-muted] .yf-voice-phase { color: var(--yf-voice-muted); }
|
|
8619
|
+
.yf-voice-print {
|
|
8620
|
+
-webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 100%);
|
|
8621
|
+
mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 100%);
|
|
8622
|
+
}
|
|
8623
|
+
.yf-voice-caption { display: flex; min-width: 0; align-items: baseline; gap: 8px; color: color-mix(in srgb, var(--yf-voice-ink) 72%, transparent); }
|
|
8624
|
+
.yf-voice-caption--javier { display: block; color: var(--yf-voice-ink); font-weight: 450; text-wrap: pretty; overflow-wrap: anywhere; }
|
|
8625
|
+
.yf-voice-hint { color: var(--yf-voice-muted); }
|
|
8626
|
+
.yf-voice-who {
|
|
8627
|
+
flex-shrink: 0;
|
|
8628
|
+
font-size: 9.5px;
|
|
8629
|
+
font-weight: 600;
|
|
8630
|
+
letter-spacing: 0.14em;
|
|
8631
|
+
text-transform: uppercase;
|
|
8632
|
+
color: var(--yf-voice-muted);
|
|
8633
|
+
}
|
|
8634
|
+
.yf-voice-deck[data-fill] { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
|
8635
|
+
.yf-voice-deck[data-fill] .yf-voice-deck__panel { flex: 1; display: flex; flex-direction: column; min-height: 0; }
|
|
8636
|
+
.yf-voice-detail {
|
|
8637
|
+
flex: 1;
|
|
8638
|
+
min-height: 0;
|
|
8639
|
+
overflow-y: auto;
|
|
8640
|
+
overflow-x: hidden;
|
|
8641
|
+
overflow-wrap: anywhere;
|
|
8642
|
+
overscroll-behavior: contain;
|
|
8643
|
+
-webkit-overflow-scrolling: touch;
|
|
8644
|
+
padding: 10px 16px 16px;
|
|
8645
|
+
border-top: 1px solid color-mix(in srgb, var(--yf-voice-ink) 8%, transparent);
|
|
8646
|
+
-webkit-mask-image: linear-gradient(180deg, transparent 0, #000 14px, #000 calc(100% - 18px), transparent 100%);
|
|
8647
|
+
mask-image: linear-gradient(180deg, transparent 0, #000 14px, #000 calc(100% - 18px), transparent 100%);
|
|
8648
|
+
animation: yf-voice-rise 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both;
|
|
8649
|
+
}
|
|
8650
|
+
.yf-voice-deck[data-phone] { border-radius: 20px; }
|
|
8651
|
+
.yf-voice-deck[data-phone] .yf-voice-deck__panel { border-radius: 19px; }
|
|
8652
|
+
.yf-voice-deck[data-phone] .yf-voice-end { height: 40px; padding: 0 16px 0 14px; font-size: 14px; }
|
|
8653
|
+
.yf-voice-deck[data-phone] .yf-voice-mute { width: 40px; height: 40px; margin-right: 2px; }
|
|
8654
|
+
.yf-voice-deck[data-phone] .yf-voice-mute svg { width: 18px; height: 18px; }
|
|
8655
|
+
.yf-voice-deck[data-phone] .yf-voice-phase { font-size: 11.5px; }
|
|
8656
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8657
|
+
.yf-voice-deck, .yf-voice-beacon, .yf-voice-deck__panel { animation: none; }
|
|
8658
|
+
}
|
|
8659
|
+
`;
|
|
8660
|
+
var STYLE_ID = "yf-voice-deck-styles";
|
|
8661
|
+
function ensureVoiceDeckStyles(doc = typeof document === "undefined" ? void 0 : document) {
|
|
8662
|
+
if (!doc || doc.getElementById(STYLE_ID)) return;
|
|
8663
|
+
const style = doc.createElement("style");
|
|
8664
|
+
style.id = STYLE_ID;
|
|
8665
|
+
style.textContent = VOICE_DECK_CSS;
|
|
8666
|
+
doc.head.appendChild(style);
|
|
8667
|
+
}
|
|
8668
|
+
function VoiceDeck({ voice, name, phone = false, hint, fill = false, children }) {
|
|
8669
|
+
const t = useTheme();
|
|
8670
|
+
React15.useEffect(() => ensureVoiceDeckStyles(), []);
|
|
8671
|
+
const now = useClock(voice.startedAt !== null);
|
|
8672
|
+
if (voice.phase === "off") return null;
|
|
8673
|
+
const speaker = name ?? voice.defaultName ?? "Assistant";
|
|
8674
|
+
const phaseWords = {
|
|
8675
|
+
connecting: "Connecting",
|
|
8676
|
+
listening: "Listening",
|
|
8677
|
+
hearing: "Hearing you",
|
|
8678
|
+
thinking: "Working",
|
|
8679
|
+
speaking: speaker
|
|
8680
|
+
};
|
|
8681
|
+
const phaseWord = voice.muted && voice.phase !== "speaking" && voice.phase !== "connecting" ? "Muted" : phaseWords[voice.phase];
|
|
8682
|
+
const { startedAt, maxSessionSecs } = voice;
|
|
8683
|
+
const elapsed = startedAt === null ? 0 : now - startedAt;
|
|
8684
|
+
const remaining = maxSessionSecs > 0 && startedAt !== null ? maxSessionSecs * 1e3 - elapsed : Infinity;
|
|
8685
|
+
const endingSoon = remaining < 6e4;
|
|
8686
|
+
const said = captionTail(voice.saying, phone ? 170 : 240);
|
|
8687
|
+
const words = captionTail(voice.heard, phone ? 90 : 140);
|
|
8688
|
+
const vars = {
|
|
8689
|
+
"--yf-voice-accent": t.primary,
|
|
8690
|
+
"--yf-voice-ink": t.text,
|
|
8691
|
+
"--yf-voice-muted": t.textMuted,
|
|
8692
|
+
"--yf-voice-panel": t.bgRaised
|
|
8693
|
+
};
|
|
8694
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yf-voice-deck", "data-phase": voice.phase, "data-phone": phone ? "" : void 0, "data-fill": fill ? "" : void 0, "data-muted": voice.muted ? "" : void 0, style: vars, children: [
|
|
8695
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "yf-voice-deck__panel", children: [
|
|
8696
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center gap-2.5 ${phone ? "px-4 pt-3.5" : "px-4 pt-3"}`, children: [
|
|
8697
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "yf-voice-beacon", "aria-hidden": "true" }),
|
|
8698
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { role: "status", "aria-live": "polite", className: "yf-voice-phase", children: phaseWord }),
|
|
8699
|
+
startedAt !== null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "yf-voice-clock", "data-soon": endingSoon ? "" : void 0, title: endingSoon ? "Voice ends soon" : "Time in this voice session", children: endingSoon && Number.isFinite(remaining) ? `ends in ${clock(remaining)}` : clock(elapsed) }) : null,
|
|
8700
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1" }),
|
|
8701
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8702
|
+
"button",
|
|
8703
|
+
{
|
|
8704
|
+
type: "button",
|
|
8705
|
+
onClick: voice.toggleMute,
|
|
8706
|
+
className: "yf-voice-mute",
|
|
8707
|
+
"aria-pressed": voice.muted,
|
|
8708
|
+
"aria-label": voice.muted ? "Unmute microphone" : "Mute microphone",
|
|
8709
|
+
title: voice.muted ? "Unmute" : "Mute",
|
|
8710
|
+
disabled: voice.phase === "connecting",
|
|
8711
|
+
children: voice.muted ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MicOff, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { "aria-hidden": "true" })
|
|
8712
|
+
}
|
|
8713
|
+
),
|
|
8714
|
+
/* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: voice.stop, className: "yf-voice-end", "aria-label": "End voice", children: [
|
|
8715
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "yf-voice-end__mark", "aria-hidden": "true" }),
|
|
8716
|
+
"End"
|
|
8717
|
+
] })
|
|
8718
|
+
] }),
|
|
8719
|
+
/* @__PURE__ */ jsxRuntime.jsx(Voiceprint, { phase: voice.phase, levels: voice.levels, height: fill ? 148 : phone ? 104 : 76, cell: fill ? 14 : phone ? 13 : 11 }),
|
|
8720
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex flex-col gap-1.5 ${phone ? "px-4 pb-4" : "px-4 pb-3.5"}`, style: { minHeight: "1.25rem" }, "aria-live": "off", children: [
|
|
8721
|
+
words ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "yf-voice-caption", style: { fontSize: phone ? 13.5 : 12.5 }, children: [
|
|
8722
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "yf-voice-who", children: "You" }),
|
|
8723
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: words })
|
|
8724
|
+
] }) : null,
|
|
8725
|
+
said ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "yf-voice-caption yf-voice-caption--javier", style: { fontSize: phone ? 16 : 14, lineHeight: phone ? 1.45 : 1.5 }, children: said }) : !words ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "yf-voice-hint", style: { fontSize: phone ? 14 : 12.5 }, children: voice.phase === "connecting" ? "Opening the line\u2026" : voice.muted ? "Your microphone is off. Tap it to talk." : hint ?? "Ask anything. Talk over me to interrupt." }) : null
|
|
8726
|
+
] }),
|
|
8727
|
+
fill && children ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "yf-voice-detail", children }) : null
|
|
8728
|
+
] }),
|
|
8729
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-voice-probe": "ink", hidden: true, style: { color: "var(--yf-voice-ink)" } }),
|
|
8730
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-voice-probe": "accent", hidden: true, style: { color: "var(--yf-voice-accent)" } }),
|
|
8731
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-voice-probe": "muted", hidden: true, style: { color: "var(--yf-voice-muted)" } })
|
|
8732
|
+
] });
|
|
8733
|
+
}
|
|
8734
|
+
function useClock(running) {
|
|
8735
|
+
const [now, setNow] = React15.useState(() => Date.now());
|
|
8736
|
+
React15.useEffect(() => {
|
|
8737
|
+
if (!running) return void 0;
|
|
8738
|
+
setNow(Date.now());
|
|
8739
|
+
const id = window.setInterval(() => setNow(Date.now()), 1e3);
|
|
8740
|
+
return () => window.clearInterval(id);
|
|
8741
|
+
}, [running]);
|
|
8742
|
+
return now;
|
|
8743
|
+
}
|
|
8744
|
+
function Voiceprint({ phase, levels, height, cell }) {
|
|
8745
|
+
const canvasRef = React15.useRef(null);
|
|
8746
|
+
const phaseRef = React15.useRef(phase);
|
|
8747
|
+
phaseRef.current = phase;
|
|
8748
|
+
const levelsRef = React15.useRef(levels);
|
|
8749
|
+
levelsRef.current = levels;
|
|
8750
|
+
React15.useEffect(() => {
|
|
8751
|
+
const canvas = canvasRef.current;
|
|
8752
|
+
const ctx = canvas?.getContext("2d");
|
|
8753
|
+
if (!canvas || !ctx) return void 0;
|
|
8754
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
8755
|
+
const cellW = Math.round(cell * 0.8);
|
|
8756
|
+
const cellH = Math.round(cell * 0.95);
|
|
8757
|
+
let width = 0;
|
|
8758
|
+
let dpr = 1;
|
|
8759
|
+
let rows = 1;
|
|
8760
|
+
let columns = [];
|
|
8761
|
+
let colors = { ink: [236, 232, 225], accent: [255, 140, 66], muted: [140, 140, 140] };
|
|
8762
|
+
let seed = 1;
|
|
8763
|
+
let last = performance.now();
|
|
8764
|
+
let colorsAt = 0;
|
|
8765
|
+
let raf = 0;
|
|
8766
|
+
const readColors = () => {
|
|
8767
|
+
const deck = canvas.closest(".yf-voice-deck");
|
|
8768
|
+
const probe = (which, fallback) => {
|
|
8769
|
+
const el = deck?.querySelector(`[data-voice-probe="${which}"]`);
|
|
8770
|
+
return el ? parseCssColor(getComputedStyle(el).color, fallback) : fallback;
|
|
8771
|
+
};
|
|
8772
|
+
colors = { ink: probe("ink", colors.ink), accent: probe("accent", colors.accent), muted: probe("muted", colors.muted) };
|
|
8773
|
+
};
|
|
8774
|
+
const resize = () => {
|
|
8775
|
+
dpr = Math.min(window.devicePixelRatio || 1, 3);
|
|
8776
|
+
width = canvas.clientWidth;
|
|
8777
|
+
canvas.width = Math.round(width * dpr);
|
|
8778
|
+
canvas.height = Math.round(height * dpr);
|
|
8779
|
+
rows = Math.max(3, Math.floor(height / cellH));
|
|
8780
|
+
const need = Math.ceil(width / cellW) + 3;
|
|
8781
|
+
columns = columns.slice(-need);
|
|
8782
|
+
while (columns.length < need) columns.unshift({ amp: 0.05, voice: 0 /* Quiet */, seed: seed++ });
|
|
8783
|
+
};
|
|
8784
|
+
const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
|
|
8785
|
+
const colorOf = (v) => v === 1 /* You */ ? colors.ink : v === 0 /* Quiet */ ? colors.muted : colors.accent;
|
|
8786
|
+
const draw = (time) => {
|
|
8787
|
+
if (time - colorsAt > 1e3) {
|
|
8788
|
+
readColors();
|
|
8789
|
+
colorsAt = time;
|
|
8790
|
+
}
|
|
8791
|
+
const step = reduced ? STEP_MS * 2.5 : STEP_MS;
|
|
8792
|
+
while (time - last >= step) {
|
|
8793
|
+
columns.shift();
|
|
8794
|
+
columns.push(nextColumn(levelsRef.current(), phaseRef.current, time, seed++));
|
|
8795
|
+
last += step;
|
|
8796
|
+
}
|
|
8797
|
+
if (time - last > step * 4) last = time;
|
|
8798
|
+
const frac = reduced ? 0 : (time - last) / step;
|
|
8799
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
8800
|
+
ctx.clearRect(0, 0, width, height);
|
|
8801
|
+
ctx.font = `500 ${cell}px "Hiragino Kaku Gothic ProN", "Noto Sans JP", ui-monospace, monospace`;
|
|
8802
|
+
ctx.textAlign = "center";
|
|
8803
|
+
ctx.textBaseline = "middle";
|
|
8804
|
+
const n = columns.length;
|
|
8805
|
+
const top = (height - rows * cellH) / 2 + cellH / 2;
|
|
8806
|
+
for (let i = 0; i < n; i += 1) {
|
|
8807
|
+
const col = columns[i];
|
|
8808
|
+
const x = width - (n - 1 - i + frac) * cellW + cellW / 2 - cellW;
|
|
8809
|
+
if (x < -cellW || x > width + cellW) continue;
|
|
8810
|
+
const fade = ageFade(x, width);
|
|
8811
|
+
const fresh = n - 1 - i < 3;
|
|
8812
|
+
const color = colorOf(col.voice);
|
|
8813
|
+
const reach = Math.min(1, col.amp * 1.15);
|
|
8814
|
+
for (let r = 0; r < rows; r += 1) {
|
|
8815
|
+
const k = cellIntensity(reach, rows, r);
|
|
8816
|
+
const s = fresh && !reduced ? col.seed + Math.floor(time / 90) : col.seed;
|
|
8817
|
+
const y = top + r * cellH;
|
|
8818
|
+
if (k <= 0.02) {
|
|
8819
|
+
ctx.fillStyle = rgba(colors.muted, 0.07 * (0.25 + 0.75 * fade));
|
|
8820
|
+
ctx.fillText(glyphFor(s + 7, r), x, y);
|
|
8821
|
+
continue;
|
|
8822
|
+
}
|
|
8823
|
+
const alpha = Math.min(1, k * (col.voice === 0 /* Quiet */ ? 0.45 : 0.95) * (0.12 + 0.88 * fade) * (fresh ? 1.3 : 1));
|
|
8824
|
+
const bright = k > 0.8 && col.voice !== 0 /* Quiet */ && !reduced;
|
|
8825
|
+
if (bright) {
|
|
8826
|
+
ctx.shadowColor = rgba(color, 0.9);
|
|
8827
|
+
ctx.shadowBlur = 7;
|
|
8828
|
+
}
|
|
8829
|
+
ctx.fillStyle = rgba(color, alpha);
|
|
8830
|
+
ctx.fillText(glyphFor(s, r), x, y);
|
|
8831
|
+
if (bright) ctx.shadowBlur = 0;
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
const head = columns[n - 1];
|
|
8835
|
+
if (head) {
|
|
8836
|
+
const c = colorOf(head.voice);
|
|
8837
|
+
const radius = height * 0.48;
|
|
8838
|
+
ctx.save();
|
|
8839
|
+
ctx.translate(width, height / 2);
|
|
8840
|
+
ctx.scale(1.9, 1);
|
|
8841
|
+
const glow = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
|
|
8842
|
+
glow.addColorStop(0, rgba(c, 0.1 + 0.3 * head.amp));
|
|
8843
|
+
glow.addColorStop(1, rgba(c, 0));
|
|
8844
|
+
ctx.fillStyle = glow;
|
|
8845
|
+
ctx.beginPath();
|
|
8846
|
+
ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
|
8847
|
+
ctx.fill();
|
|
8848
|
+
ctx.restore();
|
|
8849
|
+
const reach = Math.max(6, Math.min(1, head.amp * 1.15) * height * 0.9);
|
|
8850
|
+
const line = ctx.createLinearGradient(0, (height - reach) / 2, 0, (height + reach) / 2);
|
|
8851
|
+
line.addColorStop(0, rgba(c, 0));
|
|
8852
|
+
line.addColorStop(0.5, rgba(c, 0.5 + 0.5 * head.amp));
|
|
8853
|
+
line.addColorStop(1, rgba(c, 0));
|
|
8854
|
+
ctx.fillStyle = line;
|
|
8855
|
+
ctx.fillRect(width - 1.5, (height - reach) / 2, 1.5, reach);
|
|
8856
|
+
}
|
|
8857
|
+
raf = window.requestAnimationFrame(draw);
|
|
8858
|
+
};
|
|
8859
|
+
readColors();
|
|
8860
|
+
resize();
|
|
8861
|
+
const observer = new ResizeObserver(resize);
|
|
8862
|
+
observer.observe(canvas);
|
|
8863
|
+
raf = window.requestAnimationFrame(draw);
|
|
8864
|
+
return () => {
|
|
8865
|
+
window.cancelAnimationFrame(raf);
|
|
8866
|
+
observer.disconnect();
|
|
8867
|
+
};
|
|
8868
|
+
}, [height, cell]);
|
|
8869
|
+
return /* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: canvasRef, className: "yf-voice-print block w-full", style: { height }, "aria-hidden": "true" });
|
|
8870
|
+
}
|
|
7625
8871
|
|
|
7626
8872
|
// src/knowledge/graphTheme.ts
|
|
7627
8873
|
var NODE_TYPE_COLORS = {
|
|
@@ -14661,6 +15907,7 @@ exports.AgentAvatar = AgentAvatar;
|
|
|
14661
15907
|
exports.AgentPresenceBar = AgentPresenceBar;
|
|
14662
15908
|
exports.AgentStepTrace = AgentStepTrace;
|
|
14663
15909
|
exports.AnnotationThread = AnnotationThread;
|
|
15910
|
+
exports.AskQueue = AskQueue;
|
|
14664
15911
|
exports.CLAIM_STATUS_COLORS = CLAIM_STATUS_COLORS;
|
|
14665
15912
|
exports.ChatTerminal = ChatTerminal;
|
|
14666
15913
|
exports.CheckpointCard = CheckpointCard;
|
|
@@ -14689,19 +15936,28 @@ exports.NODE_TYPE_LABELS = NODE_TYPE_LABELS;
|
|
|
14689
15936
|
exports.NodeDetailDrawer = NodeDetailDrawer;
|
|
14690
15937
|
exports.PROGRESS_EVENT_NAME = PROGRESS_EVENT_NAME;
|
|
14691
15938
|
exports.PipelineProgressBar = PipelineProgressBar;
|
|
15939
|
+
exports.REPLACED_OUTPUT = REPLACED_OUTPUT;
|
|
14692
15940
|
exports.ReasoningHistoryCard = ReasoningHistoryCard;
|
|
14693
15941
|
exports.ReasoningOverlay = ReasoningOverlay;
|
|
14694
15942
|
exports.ReasoningView = ReasoningView;
|
|
15943
|
+
exports.SILENT = SILENT;
|
|
14695
15944
|
exports.SSE_KEEPALIVE_MS = SSE_KEEPALIVE_MS;
|
|
14696
15945
|
exports.SSE_STALL_MS = SSE_STALL_MS;
|
|
14697
15946
|
exports.SYNTHESIS_DONE_SIGNAL = SYNTHESIS_DONE_SIGNAL;
|
|
14698
15947
|
exports.ScopedReasoningProvider = ScopedReasoningProvider;
|
|
15948
|
+
exports.ScreenAwake = ScreenAwake;
|
|
14699
15949
|
exports.SettlementBar = SettlementBar;
|
|
14700
15950
|
exports.SlashMentionPicker = SlashMentionPicker;
|
|
14701
15951
|
exports.StageCanvas = StageCanvas;
|
|
14702
15952
|
exports.StandardTerminal = StandardTerminal;
|
|
14703
15953
|
exports.TerminalInputBar = TerminalInputBar;
|
|
14704
15954
|
exports.TerminalThemeProvider = TerminalThemeProvider;
|
|
15955
|
+
exports.VOICE_CAPTURE_WORKLET_SOURCE = VOICE_CAPTURE_WORKLET_SOURCE;
|
|
15956
|
+
exports.VOICE_LANGUAGES = VOICE_LANGUAGES;
|
|
15957
|
+
exports.VOICE_NAMES = VOICE_NAMES;
|
|
15958
|
+
exports.VoiceDeck = VoiceDeck;
|
|
15959
|
+
exports.VoiceInputButton = VoiceInputButton;
|
|
15960
|
+
exports.VoiceSession = VoiceSession;
|
|
14705
15961
|
exports.agentSeed = agentSeed;
|
|
14706
15962
|
exports.agentStepTraceExtra = agentStepTraceExtra;
|
|
14707
15963
|
exports.avatarColor = avatarColor;
|
|
@@ -14715,6 +15971,7 @@ exports.dispatchProgressEvent = dispatchProgressEvent;
|
|
|
14715
15971
|
exports.documentEventToActivity = documentEventToActivity;
|
|
14716
15972
|
exports.emeraldCollab = emeraldCollab;
|
|
14717
15973
|
exports.emeraldRevised = emeraldRevised;
|
|
15974
|
+
exports.encodePersona = encodePersona;
|
|
14718
15975
|
exports.eventToActivity = eventToActivity;
|
|
14719
15976
|
exports.extractKGSources = extractKGSources;
|
|
14720
15977
|
exports.extractKgId = extractKgId;
|
|
@@ -14731,6 +15988,7 @@ exports.getNodeColors = getNodeColors;
|
|
|
14731
15988
|
exports.getNodeTypeLabel = getNodeTypeLabel;
|
|
14732
15989
|
exports.getVocabTypeColors = getVocabTypeColors;
|
|
14733
15990
|
exports.getVocabTypeLabel = getVocabTypeLabel;
|
|
15991
|
+
exports.getVoiceStatus = getVoiceStatus;
|
|
14734
15992
|
exports.greenAction = greenAction;
|
|
14735
15993
|
exports.handleSSEStream = handleSSEStream;
|
|
14736
15994
|
exports.handleStreamingResponse = handleStreamingResponse;
|
|
@@ -14750,10 +16008,14 @@ exports.reduceProgress = reduceProgress;
|
|
|
14750
16008
|
exports.renderMarkdownBlock = renderMarkdownBlock;
|
|
14751
16009
|
exports.resolvePaymentAssetDisplay = resolvePaymentAssetDisplay;
|
|
14752
16010
|
exports.snapshotToSubgraph = snapshotToSubgraph;
|
|
16011
|
+
exports.socketBase = socketBase;
|
|
14753
16012
|
exports.terminalClassNames = terminalClassNames;
|
|
14754
16013
|
exports.terminalColors = terminalColors;
|
|
14755
16014
|
exports.useScopedProgressEvent = useScopedProgressEvent;
|
|
14756
16015
|
exports.useSlashMentionPicker = useSlashMentionPicker;
|
|
14757
16016
|
exports.useTheme = useTheme;
|
|
16017
|
+
exports.useVoiceSession = useVoiceSession;
|
|
16018
|
+
exports.voiceEndedMessage = voiceEndedMessage;
|
|
16019
|
+
exports.voiceSocketUrl = voiceSocketUrl;
|
|
14758
16020
|
exports.warmBg = warmBg;
|
|
14759
16021
|
exports.withStallTimeout = withStallTimeout;
|