@yieldfabric/terminal 1.1.1 → 1.2.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.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Runtime use requires a separate express license.
7
7
  */
8
8
  import React15, { createContext, memo, useMemo, useRef, useEffect, useCallback, useContext, useState, useReducer } from 'react';
9
- import { X, Check, Plus, FileText, MessageCircle, ChevronRight, Paperclip, FolderOpen, Brain, XCircle, Loader2, Minimize2, Maximize2, AlertTriangle, HelpCircle, ChevronLeft, Pencil, ChevronDown, ExternalLink, Search, Network, Boxes, Settings, Terminal, CheckCircle, ArrowRight, Users, Eye, AlertCircle, CheckCircle2, Copy, Zap, Download, Trash2, Link2 } from 'lucide-react';
9
+ import { X, Check, Plus, FileText, MessageCircle, ChevronRight, Paperclip, FolderOpen, Brain, XCircle, Loader2, Minimize2, Maximize2, Mic, AlertTriangle, HelpCircle, ChevronLeft, Pencil, ChevronDown, ExternalLink, Search, Network, Boxes, Settings, Terminal, CheckCircle, ArrowRight, Users, Eye, AlertCircle, CheckCircle2, Copy, Zap, Download, Trash2, Link2 } from 'lucide-react';
10
10
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
  import ReactMarkdown from 'react-markdown';
12
12
  import remarkGfm from 'remark-gfm';
@@ -809,11 +809,11 @@ var AVATAR_COLORS = [
809
809
  "#ef4444"
810
810
  ];
811
811
  function avatarColor(entityId) {
812
- let hash = 0;
812
+ let hash2 = 0;
813
813
  for (let i = 0; i < entityId.length; i++) {
814
- hash = entityId.charCodeAt(i) + ((hash << 5) - hash);
814
+ hash2 = entityId.charCodeAt(i) + ((hash2 << 5) - hash2);
815
815
  }
816
- return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
816
+ return AVATAR_COLORS[Math.abs(hash2) % AVATAR_COLORS.length];
817
817
  }
818
818
  function initials(name) {
819
819
  return name.split(" ").map((w) => w[0]).join("").toUpperCase().slice(0, 2);
@@ -3380,6 +3380,7 @@ function TerminalInputBar({
3380
3380
  selectedPaletteItem,
3381
3381
  onSelectPaletteItem,
3382
3382
  inputBarExtra,
3383
+ leading,
3383
3384
  modeSelector,
3384
3385
  compactToolbar = false,
3385
3386
  submitTitle = "Send",
@@ -3658,6 +3659,7 @@ function TerminalInputBar({
3658
3659
  })(),
3659
3660
  /* @__PURE__ */ jsxs("div", { ref: toolbarRef, className: "flex items-center justify-between pt-2.5 gap-2", children: [
3660
3661
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-[11px] min-w-0 flex-wrap", children: [
3662
+ leading,
3661
3663
  chipsVisible && /* @__PURE__ */ jsxs(Fragment, { children: [
3662
3664
  /* @__PURE__ */ jsx(
3663
3665
  "button",
@@ -3668,7 +3670,7 @@ function TerminalInputBar({
3668
3670
  inputRef.current?.focus();
3669
3671
  },
3670
3672
  title: "Insert @ mention",
3671
- className: `${toolbarIsCompact ? "px-2" : "px-2.5"} py-1 min-h-[40px] sm:min-h-0 inline-flex items-center rounded-lg transition-colors font-semibold flex-shrink-0`,
3673
+ className: `${toolbarIsCompact ? "px-2" : "px-2.5"} h-[22px] inline-flex items-center rounded-lg transition-colors font-semibold flex-shrink-0`,
3672
3674
  style: toolbarButtonStyle,
3673
3675
  onMouseEnter: (e) => {
3674
3676
  e.currentTarget.style.backgroundColor = toolbarButtonHover;
@@ -3688,7 +3690,7 @@ function TerminalInputBar({
3688
3690
  inputRef.current?.focus();
3689
3691
  },
3690
3692
  title: "Insert / command",
3691
- className: `${toolbarIsCompact ? "px-2" : "px-2.5"} py-1 min-h-[40px] sm:min-h-0 inline-flex items-center rounded-lg transition-colors font-semibold flex-shrink-0`,
3693
+ className: `${toolbarIsCompact ? "px-2" : "px-2.5"} h-[22px] inline-flex items-center rounded-lg transition-colors font-semibold flex-shrink-0`,
3692
3694
  style: toolbarButtonStyle,
3693
3695
  onMouseEnter: (e) => {
3694
3696
  e.currentTarget.style.backgroundColor = toolbarButtonHover;
@@ -3722,7 +3724,7 @@ function TerminalInputBar({
3722
3724
  {
3723
3725
  type: "button",
3724
3726
  disabled: isExecuting || disabled,
3725
- className: "h-10 w-10 sm:h-8 sm:w-8 rounded-xl disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200",
3727
+ className: "h-[22px] px-1.5 rounded-lg disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200",
3726
3728
  style: toolbarButtonStyle,
3727
3729
  title: "Upload files",
3728
3730
  onClick: () => fileInputRef.current?.click(),
@@ -3732,7 +3734,7 @@ function TerminalInputBar({
3732
3734
  onMouseLeave: (e) => {
3733
3735
  e.currentTarget.style.backgroundColor = toolbarButtonBg;
3734
3736
  },
3735
- children: /* @__PURE__ */ jsx(Paperclip, { className: "w-4 h-4" })
3737
+ children: /* @__PURE__ */ jsx(Paperclip, { className: "w-3.5 h-3.5" })
3736
3738
  }
3737
3739
  ),
3738
3740
  showFilesButton && /* @__PURE__ */ jsx(
@@ -3740,7 +3742,7 @@ function TerminalInputBar({
3740
3742
  {
3741
3743
  type: "button",
3742
3744
  disabled: isExecuting || disabled,
3743
- className: "h-10 w-10 sm:h-8 sm:w-8 rounded-xl disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200",
3745
+ className: "h-[22px] px-1.5 rounded-lg disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200",
3744
3746
  style: toolbarButtonStyle,
3745
3747
  title: "List files",
3746
3748
  onClick: () => onListFilesCommand(),
@@ -3750,7 +3752,7 @@ function TerminalInputBar({
3750
3752
  onMouseLeave: (e) => {
3751
3753
  e.currentTarget.style.backgroundColor = toolbarButtonBg;
3752
3754
  },
3753
- children: /* @__PURE__ */ jsx(FolderOpen, { className: "w-4 h-4" })
3755
+ children: /* @__PURE__ */ jsx(FolderOpen, { className: "w-3.5 h-3.5" })
3754
3756
  }
3755
3757
  )
3756
3758
  ] })
@@ -5538,6 +5540,7 @@ Path: ${parsed.endpoint.path}`);
5538
5540
  core.setCommandPaletteItems([]);
5539
5541
  },
5540
5542
  inputBarExtra: slots?.inputBarExtra,
5543
+ leading: slots?.inputBarLeading,
5541
5544
  modeSelector: slots?.modeSelector
5542
5545
  }
5543
5546
  ),
@@ -6210,6 +6213,7 @@ function ChatTerminal({
6210
6213
  core.setCommandPaletteItems([]);
6211
6214
  },
6212
6215
  inputBarExtra: slots?.inputBarExtra,
6216
+ leading: slots?.inputBarLeading,
6213
6217
  modeSelector: slots?.modeSelector
6214
6218
  }
6215
6219
  ),
@@ -7613,6 +7617,1104 @@ function agentStepTraceExtra(entry) {
7613
7617
  const working = meta?.agentStepsLive ?? entry.content.endsWith("\u258B");
7614
7618
  return /* @__PURE__ */ jsx(AgentStepTrace, { steps, working });
7615
7619
  }
7620
+ var BAR_SHAPE = [0.55, 1, 0.8, 0.45];
7621
+ function tintOf(color, alpha) {
7622
+ return `color-mix(in srgb, ${color} ${Math.round(alpha * 100)}%, transparent)`;
7623
+ }
7624
+ function useCoarsePointer() {
7625
+ const [coarse, setCoarse] = useState(
7626
+ () => typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches
7627
+ );
7628
+ useEffect(() => {
7629
+ const mq = window.matchMedia?.("(pointer: coarse)");
7630
+ if (!mq) return void 0;
7631
+ const on = () => setCoarse(mq.matches);
7632
+ mq.addEventListener?.("change", on);
7633
+ return () => mq.removeEventListener?.("change", on);
7634
+ }, []);
7635
+ return coarse;
7636
+ }
7637
+ function VoiceInputButton({
7638
+ state,
7639
+ onToggle,
7640
+ disabled = false,
7641
+ level,
7642
+ startTitle = "Talk",
7643
+ stopTitle = "End voice"
7644
+ }) {
7645
+ const t = useTheme();
7646
+ const coarse = useCoarsePointer();
7647
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
7648
+ const on = state !== "off";
7649
+ const tint = (a) => tintOf(t.primary, a);
7650
+ const bars = useRef([]);
7651
+ const levelRef = useRef(level);
7652
+ levelRef.current = level;
7653
+ useEffect(() => {
7654
+ if (state !== "live") return void 0;
7655
+ let raf = 0;
7656
+ let smooth = 0;
7657
+ const tick = (now) => {
7658
+ const raw = levelRef.current ? Math.min(1, Math.max(0, levelRef.current())) : 0;
7659
+ smooth = raw > smooth ? smooth + (raw - smooth) * 0.55 : smooth + (raw - smooth) * 0.12;
7660
+ bars.current.forEach((bar, i) => {
7661
+ if (!bar) return;
7662
+ const idle = 0.18 + 0.1 * Math.sin(now / 320 + i * 1.3);
7663
+ const h = Math.max(idle, smooth * BAR_SHAPE[i] * (0.85 + 0.15 * Math.sin(now / 90 + i * 2.1)));
7664
+ bar.style.transform = `scaleY(${Math.min(1, h).toFixed(3)})`;
7665
+ });
7666
+ raf = window.requestAnimationFrame(tick);
7667
+ };
7668
+ raf = window.requestAnimationFrame(tick);
7669
+ return () => window.cancelAnimationFrame(raf);
7670
+ }, [state]);
7671
+ const size = coarse ? 32 : 22;
7672
+ const title = on ? stopTitle : startTitle;
7673
+ const style = on ? {
7674
+ backgroundColor: t.primary,
7675
+ border: `1px solid ${t.primary}`,
7676
+ color: t.textInverse,
7677
+ boxShadow: `0 0 0 3px ${tint(0.18)}, 0 6px 18px -6px ${tint(0.75)}`
7678
+ } : {
7679
+ backgroundColor: isLight ? tint(0.07) : tint(0.1),
7680
+ border: `1px solid ${tint(isLight ? 0.35 : 0.3)}`,
7681
+ color: t.primary
7682
+ };
7683
+ return /* @__PURE__ */ jsxs(
7684
+ "button",
7685
+ {
7686
+ type: "button",
7687
+ disabled: disabled && !on,
7688
+ "aria-pressed": on,
7689
+ "aria-label": title,
7690
+ title,
7691
+ 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"}`,
7692
+ style: { ...style, height: size, minWidth: size, WebkitTapHighlightColor: "transparent" },
7693
+ onClick: onToggle,
7694
+ children: [
7695
+ state === "live" ? /* @__PURE__ */ jsx(
7696
+ "span",
7697
+ {
7698
+ "aria-hidden": "true",
7699
+ className: `pointer-events-none absolute inset-0 animate-ping ${coarse ? "rounded-full" : "rounded-lg"}`,
7700
+ style: { backgroundColor: tint(0.35), animationDuration: "1.8s" }
7701
+ }
7702
+ ) : null,
7703
+ state === "connecting" ? /* @__PURE__ */ jsx(Loader2, { className: coarse ? "w-4 h-4 animate-spin" : "w-3.5 h-3.5 animate-spin" }) : state === "live" ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "relative flex items-center gap-[2px]", style: { height: coarse ? 14 : 11 }, children: BAR_SHAPE.map((_, i) => /* @__PURE__ */ jsx(
7704
+ "span",
7705
+ {
7706
+ ref: (el) => {
7707
+ bars.current[i] = el;
7708
+ },
7709
+ className: "block rounded-full",
7710
+ style: {
7711
+ width: coarse ? 2.5 : 2,
7712
+ height: "100%",
7713
+ backgroundColor: "currentColor",
7714
+ transform: "scaleY(0.25)",
7715
+ transformOrigin: "center",
7716
+ transition: "transform 60ms linear"
7717
+ }
7718
+ },
7719
+ i
7720
+ )) }) : /* @__PURE__ */ jsx(Mic, { className: coarse ? "w-4 h-4" : "w-3.5 h-3.5" })
7721
+ ]
7722
+ }
7723
+ );
7724
+ }
7725
+
7726
+ // src/voice/askQueue.ts
7727
+ var REPLACED_OUTPUT = "Replaced by their corrected question, which is being answered instead. Say nothing about this one.";
7728
+ var AskQueue = class {
7729
+ /**
7730
+ * `run` asks the host (resolves with what the voice speaks from; a
7731
+ * rejection is reported as a failure). `finish` sends the result: `speak`
7732
+ * false for a replaced question.
7733
+ */
7734
+ constructor(run, finish) {
7735
+ this.run = run;
7736
+ this.finish = finish;
7737
+ this.waiting = [];
7738
+ this.running = null;
7739
+ }
7740
+ /** Questions being worked on or waiting. */
7741
+ get size() {
7742
+ return this.waiting.length + (this.running ? 1 : 0);
7743
+ }
7744
+ add(ask) {
7745
+ if (ask.replaces && this.size > 0) {
7746
+ for (const dropped of this.waiting) this.finish(dropped, REPLACED_OUTPUT, false);
7747
+ this.waiting = [ask];
7748
+ this.running?.controller.abort();
7749
+ } else {
7750
+ this.waiting.push(ask);
7751
+ }
7752
+ this.next();
7753
+ }
7754
+ next() {
7755
+ if (this.running) return;
7756
+ const ask = this.waiting.shift();
7757
+ if (!ask) return;
7758
+ const controller = new AbortController();
7759
+ this.running = { ask, controller };
7760
+ 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) => {
7761
+ this.running = null;
7762
+ if (controller.signal.aborted) this.finish(ask, REPLACED_OUTPUT, false);
7763
+ else this.finish(ask, output, true);
7764
+ this.next();
7765
+ });
7766
+ }
7767
+ };
7768
+
7769
+ // src/voice/pcm.ts
7770
+ var WIRE_RATE = 24e3;
7771
+ var FRAME_SAMPLES = 2400;
7772
+ var Downsampler = class {
7773
+ constructor(inputRate, outputRate = WIRE_RATE) {
7774
+ /** Input samples not yet consumed. */
7775
+ this.carry = new Float32Array(0);
7776
+ /** Fractional read position into `carry`. */
7777
+ this.pos = 0;
7778
+ if (!(inputRate > 0) || !(outputRate > 0)) throw new Error("Sample rates must be positive");
7779
+ this.ratio = inputRate / outputRate;
7780
+ }
7781
+ push(input) {
7782
+ const buf = new Float32Array(this.carry.length + input.length);
7783
+ buf.set(this.carry, 0);
7784
+ buf.set(input, this.carry.length);
7785
+ const out = [];
7786
+ let pos = this.pos;
7787
+ while (pos + this.ratio <= buf.length) {
7788
+ const start = Math.floor(pos);
7789
+ const end = Math.max(start + 1, Math.floor(pos + this.ratio));
7790
+ let sum = 0;
7791
+ for (let i = start; i < end; i += 1) sum += buf[i];
7792
+ out.push(floatToInt16(sum / (end - start)));
7793
+ pos += this.ratio;
7794
+ }
7795
+ const consumed = Math.floor(pos);
7796
+ this.carry = buf.slice(consumed);
7797
+ this.pos = pos - consumed;
7798
+ return Int16Array.from(out);
7799
+ }
7800
+ };
7801
+ function floatToInt16(sample) {
7802
+ const s = Math.max(-1, Math.min(1, Number.isFinite(sample) ? sample : 0));
7803
+ return s < 0 ? Math.round(s * 32768) : Math.round(s * 32767);
7804
+ }
7805
+ var FrameBuffer = class {
7806
+ constructor() {
7807
+ this.pending = [];
7808
+ }
7809
+ push(samples) {
7810
+ for (let i = 0; i < samples.length; i += 1) this.pending.push(samples[i]);
7811
+ const frames = [];
7812
+ while (this.pending.length >= FRAME_SAMPLES) {
7813
+ frames.push(toLittleEndian(this.pending.splice(0, FRAME_SAMPLES)));
7814
+ }
7815
+ return frames;
7816
+ }
7817
+ };
7818
+ function toLittleEndian(samples) {
7819
+ const view = new DataView(new ArrayBuffer(samples.length * 2));
7820
+ samples.forEach((s, i) => view.setInt16(i * 2, s, true));
7821
+ return view.buffer;
7822
+ }
7823
+ function pcm16ToFloat32(bytes) {
7824
+ const view = new DataView(bytes);
7825
+ const out = new Float32Array(Math.floor(bytes.byteLength / 2));
7826
+ for (let i = 0; i < out.length; i += 1) out[i] = view.getInt16(i * 2, true) / 32768;
7827
+ return out;
7828
+ }
7829
+
7830
+ // src/voice/persona.ts
7831
+ var VOICE_NAMES = [
7832
+ "alloy",
7833
+ "ash",
7834
+ "ballad",
7835
+ "cedar",
7836
+ "coral",
7837
+ "echo",
7838
+ "marin",
7839
+ "sage",
7840
+ "shimmer",
7841
+ "verse"
7842
+ ];
7843
+ var VOICE_LANGUAGES = ["en", "es", "pt", "fr", "de", "it", "nl", "ar", "hi", "ja", "ko", "zh"];
7844
+ function encodePersona(persona) {
7845
+ if (!persona) return null;
7846
+ const fields = Object.fromEntries(Object.entries(persona).filter(([, v]) => v !== void 0));
7847
+ return Object.keys(fields).length ? JSON.stringify(fields) : null;
7848
+ }
7849
+
7850
+ // src/voice/worklet.ts
7851
+ var VOICE_CAPTURE_WORKLET_SOURCE = `class YfVoiceCapture extends AudioWorkletProcessor {
7852
+ constructor() {
7853
+ super();
7854
+ this.batch = new Float32Array(2048);
7855
+ this.filled = 0;
7856
+ }
7857
+ process(inputs) {
7858
+ const channel = inputs[0] && inputs[0][0];
7859
+ if (channel) {
7860
+ let offset = 0;
7861
+ while (offset < channel.length) {
7862
+ const n = Math.min(channel.length - offset, this.batch.length - this.filled);
7863
+ this.batch.set(channel.subarray(offset, offset + n), this.filled);
7864
+ this.filled += n;
7865
+ offset += n;
7866
+ if (this.filled === this.batch.length) {
7867
+ this.port.postMessage(this.batch, [this.batch.buffer]);
7868
+ this.batch = new Float32Array(2048);
7869
+ this.filled = 0;
7870
+ }
7871
+ }
7872
+ }
7873
+ return true;
7874
+ }
7875
+ }
7876
+ registerProcessor('yf-voice-capture', YfVoiceCapture);
7877
+ `;
7878
+ var VOICE_CAPTURE_PROCESSOR = "yf-voice-capture";
7879
+
7880
+ // src/voice/session.ts
7881
+ function socketBase(base) {
7882
+ return base.replace(/^http/i, "ws").replace(/\/+$/, "");
7883
+ }
7884
+ var OFF = { enabled: false, blocked: null, maxSessionSecs: 0, persona: null };
7885
+ async function getVoiceStatus(connection, signal) {
7886
+ const token = await connection.getToken();
7887
+ if (!token) return OFF;
7888
+ try {
7889
+ const res = await fetch(`${connection.baseUrl.replace(/\/+$/, "")}/voice/status`, {
7890
+ headers: { Authorization: `Bearer ${token}` },
7891
+ signal
7892
+ });
7893
+ if (!res.ok) return OFF;
7894
+ const body = await res.json();
7895
+ return {
7896
+ enabled: body?.enabled === true,
7897
+ blocked: typeof body?.blocked === "string" ? body.blocked : null,
7898
+ maxSessionSecs: Number(body?.max_session_secs) || 0,
7899
+ persona: body?.persona && typeof body.persona.name === "string" ? body.persona : null
7900
+ };
7901
+ } catch {
7902
+ return OFF;
7903
+ }
7904
+ }
7905
+ var SILENT = { input: 0, output: 0 };
7906
+ function rms(analyser, buf) {
7907
+ if (!analyser || !buf) return 0;
7908
+ analyser.getFloatTimeDomainData(buf);
7909
+ let sum = 0;
7910
+ for (let i = 0; i < buf.length; i += 1) sum += buf[i] * buf[i];
7911
+ return Math.sqrt(sum / buf.length);
7912
+ }
7913
+ var PcmPlayer = class {
7914
+ constructor(ctx, out, onIdle) {
7915
+ this.ctx = ctx;
7916
+ this.out = out;
7917
+ this.onIdle = onIdle;
7918
+ this.next = 0;
7919
+ this.live = /* @__PURE__ */ new Set();
7920
+ }
7921
+ get playing() {
7922
+ return this.live.size > 0;
7923
+ }
7924
+ enqueue(bytes) {
7925
+ const samples = pcm16ToFloat32(bytes);
7926
+ if (samples.length === 0) return;
7927
+ const buffer = this.ctx.createBuffer(1, samples.length, WIRE_RATE);
7928
+ buffer.getChannelData(0).set(samples);
7929
+ const source = this.ctx.createBufferSource();
7930
+ source.buffer = buffer;
7931
+ source.connect(this.out);
7932
+ const at = Math.max(this.ctx.currentTime + 0.02, this.next);
7933
+ source.start(at);
7934
+ this.next = at + buffer.duration;
7935
+ this.live.add(source);
7936
+ source.onended = () => {
7937
+ this.live.delete(source);
7938
+ if (this.live.size === 0) this.onIdle();
7939
+ };
7940
+ }
7941
+ stop() {
7942
+ this.live.forEach((source) => {
7943
+ source.onended = null;
7944
+ try {
7945
+ source.stop();
7946
+ } catch {
7947
+ }
7948
+ });
7949
+ this.live.clear();
7950
+ this.next = 0;
7951
+ }
7952
+ };
7953
+ var FINISH_GRACE_MS = 800;
7954
+ var FINISH_MAX_WAIT_MS = 15e3;
7955
+ var CLOSE_REASONS = {
7956
+ time_limit: "time_limit",
7957
+ replaced: "replaced",
7958
+ upstream_closed: "lost",
7959
+ upstream_error: "lost"
7960
+ };
7961
+ function voiceSocketUrl(connection, token) {
7962
+ const params = new URLSearchParams({ access_token: token });
7963
+ if (connection.threadId) params.set("thread_id", connection.threadId);
7964
+ if (connection.workingGroupId) params.set("working_group_id", connection.workingGroupId);
7965
+ const persona = encodePersona(connection.persona);
7966
+ if (persona) params.set("persona", persona);
7967
+ return `${socketBase(connection.baseUrl)}/voice/realtime?${params.toString()}`;
7968
+ }
7969
+ var VoiceSession = class {
7970
+ constructor(h, connection) {
7971
+ this.h = h;
7972
+ this.connection = connection;
7973
+ this.ctx = null;
7974
+ this.stream = null;
7975
+ this.node = null;
7976
+ this.ws = null;
7977
+ this.player = null;
7978
+ this.inputAnalyser = null;
7979
+ this.outputAnalyser = null;
7980
+ this.inputBuf = null;
7981
+ this.outputBuf = null;
7982
+ this.ready = false;
7983
+ this.ended = false;
7984
+ this.phase = null;
7985
+ /** The voice's questions to the host, one at a time (see `askQueue.ts`). */
7986
+ this.asks = new AskQueue(
7987
+ (ask, signal) => {
7988
+ let open = true;
7989
+ const progress = (line, kind) => {
7990
+ if (open && !signal.aborted && line.trim()) this.send({ type: "progress", kind, text: line.slice(0, 240) });
7991
+ };
7992
+ return this.h.onAsk(ask.question, ask.heard, ask.utterance, progress, signal).finally(() => {
7993
+ open = false;
7994
+ });
7995
+ },
7996
+ (ask, output, speak) => {
7997
+ if (this.ended) return;
7998
+ this.send({ type: "tool_result", call_id: ask.callId, output, ...speak ? {} : { speak: false } });
7999
+ this.settle();
8000
+ }
8001
+ );
8002
+ this.heard = null;
8003
+ this.partialHeard = "";
8004
+ this.saying = "";
8005
+ this.endingReason = null;
8006
+ this.finishTimer = null;
8007
+ this.blobUrl = null;
8008
+ }
8009
+ /** Call from the tap itself: iOS only lets audio start inside a gesture,
8010
+ * so the AudioContext is created before the first `await`. */
8011
+ async start() {
8012
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
8013
+ if (!Ctx || !navigator.mediaDevices?.getUserMedia || typeof AudioWorkletNode === "undefined") {
8014
+ this.end("unavailable", "This browser cannot do live voice.");
8015
+ return;
8016
+ }
8017
+ const ctx = new Ctx();
8018
+ this.ctx = ctx;
8019
+ void ctx.resume();
8020
+ this.setPhase("connecting");
8021
+ const status = await getVoiceStatus(this.connection);
8022
+ if (this.ended) return;
8023
+ if (status.blocked) {
8024
+ this.end("blocked", status.blocked);
8025
+ return;
8026
+ }
8027
+ if (!status.enabled) {
8028
+ this.end("unavailable", "Voice is not switched on.");
8029
+ return;
8030
+ }
8031
+ try {
8032
+ this.stream = await navigator.mediaDevices.getUserMedia({
8033
+ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true, autoGainControl: true }
8034
+ });
8035
+ } catch (e) {
8036
+ this.end("mic_denied", e?.message);
8037
+ return;
8038
+ }
8039
+ if (this.ended) {
8040
+ this.release();
8041
+ return;
8042
+ }
8043
+ try {
8044
+ let url = this.connection.workletUrl;
8045
+ if (!url) {
8046
+ this.blobUrl = URL.createObjectURL(new Blob([VOICE_CAPTURE_WORKLET_SOURCE], { type: "application/javascript" }));
8047
+ url = this.blobUrl;
8048
+ }
8049
+ await ctx.audioWorklet.addModule(url);
8050
+ } catch (e) {
8051
+ this.end("unavailable", e?.message);
8052
+ return;
8053
+ }
8054
+ if (this.ended) return;
8055
+ const down = new Downsampler(ctx.sampleRate);
8056
+ const frames = new FrameBuffer();
8057
+ const node = new AudioWorkletNode(ctx, VOICE_CAPTURE_PROCESSOR);
8058
+ this.node = node;
8059
+ node.port.onmessage = (e) => {
8060
+ const ws2 = this.ws;
8061
+ if (!this.ready || !ws2 || ws2.readyState !== WebSocket.OPEN) return;
8062
+ for (const frame of frames.push(down.push(e.data))) ws2.send(frame);
8063
+ };
8064
+ const mic = ctx.createMediaStreamSource(this.stream);
8065
+ const inputAnalyser = ctx.createAnalyser();
8066
+ inputAnalyser.fftSize = 1024;
8067
+ const outputAnalyser = ctx.createAnalyser();
8068
+ outputAnalyser.fftSize = 1024;
8069
+ this.inputAnalyser = inputAnalyser;
8070
+ this.outputAnalyser = outputAnalyser;
8071
+ this.inputBuf = new Float32Array(inputAnalyser.fftSize);
8072
+ this.outputBuf = new Float32Array(outputAnalyser.fftSize);
8073
+ const mute = ctx.createGain();
8074
+ mute.gain.value = 0;
8075
+ mic.connect(inputAnalyser);
8076
+ inputAnalyser.connect(node);
8077
+ node.connect(mute);
8078
+ mute.connect(ctx.destination);
8079
+ outputAnalyser.connect(ctx.destination);
8080
+ this.player = new PcmPlayer(ctx, outputAnalyser, () => this.settle());
8081
+ const token = await this.connection.getToken();
8082
+ if (this.ended) return;
8083
+ if (!token) {
8084
+ this.end("unavailable", "Sign in again to use voice.");
8085
+ return;
8086
+ }
8087
+ const ws = new WebSocket(voiceSocketUrl(this.connection, token));
8088
+ ws.binaryType = "arraybuffer";
8089
+ this.ws = ws;
8090
+ ws.onmessage = (e) => {
8091
+ if (typeof e.data === "string") this.onEvent(e.data);
8092
+ else this.onAudio(e.data);
8093
+ };
8094
+ ws.onclose = (e) => {
8095
+ if (this.ended) return;
8096
+ if (!this.ready) {
8097
+ this.end("unavailable");
8098
+ return;
8099
+ }
8100
+ this.end(this.endingReason ?? CLOSE_REASONS[e.reason] ?? "lost");
8101
+ };
8102
+ }
8103
+ stop() {
8104
+ this.end("stopped");
8105
+ }
8106
+ /** Cheap enough to call every animation frame. */
8107
+ levels() {
8108
+ if (this.ended) return SILENT;
8109
+ return {
8110
+ input: rms(this.inputAnalyser, this.inputBuf),
8111
+ output: this.player?.playing ? rms(this.outputAnalyser, this.outputBuf) : 0
8112
+ };
8113
+ }
8114
+ setPhase(phase) {
8115
+ if (this.ended || this.phase === phase) return;
8116
+ this.phase = phase;
8117
+ this.h.onPhase(phase);
8118
+ }
8119
+ settle() {
8120
+ if (this.asks.size > 0) this.setPhase("thinking");
8121
+ else if (this.player?.playing) this.setPhase("speaking");
8122
+ else this.setPhase("listening");
8123
+ }
8124
+ send(message) {
8125
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(message));
8126
+ }
8127
+ onAudio(bytes) {
8128
+ if (this.ended || !this.player) return;
8129
+ this.player.enqueue(bytes);
8130
+ this.setPhase("speaking");
8131
+ }
8132
+ onEvent(raw) {
8133
+ let ev;
8134
+ try {
8135
+ ev = JSON.parse(raw);
8136
+ } catch {
8137
+ return;
8138
+ }
8139
+ const text = (k) => typeof ev[k] === "string" ? ev[k] : "";
8140
+ switch (ev.type) {
8141
+ case "session.ready":
8142
+ this.ready = true;
8143
+ this.setPhase("listening");
8144
+ break;
8145
+ case "input_audio_buffer.speech_started":
8146
+ this.cancelFinish();
8147
+ this.player?.stop();
8148
+ this.saying = "";
8149
+ this.partialHeard = "";
8150
+ this.heard = null;
8151
+ this.setPhase("hearing");
8152
+ break;
8153
+ case "input_audio_buffer.speech_stopped":
8154
+ this.setPhase("thinking");
8155
+ break;
8156
+ case "conversation.item.input_audio_transcription.delta":
8157
+ this.partialHeard += text("delta");
8158
+ this.h.onHeard(this.partialHeard, false);
8159
+ break;
8160
+ case "conversation.item.input_audio_transcription.completed":
8161
+ this.heard = text("transcript").trim() || null;
8162
+ this.partialHeard = "";
8163
+ this.h.onHeard(this.heard ?? "", true);
8164
+ if (this.heard && text("message_id")) {
8165
+ this.h.onUtterance?.({ itemId: text("item_id"), messageId: text("message_id"), text: this.heard });
8166
+ }
8167
+ break;
8168
+ case "voice.reply":
8169
+ if (text("message_id") && text("text")) {
8170
+ this.h.onReply?.({
8171
+ messageId: text("message_id"),
8172
+ itemId: text("item_id") || null,
8173
+ text: text("text"),
8174
+ interrupted: ev.interrupted === true
8175
+ });
8176
+ }
8177
+ break;
8178
+ case "response.audio_transcript.delta":
8179
+ this.saying += text("delta");
8180
+ this.h.onSaying(this.saying, false);
8181
+ break;
8182
+ case "response.audio_transcript.done":
8183
+ this.h.onSaying(text("transcript") || this.saying, true);
8184
+ this.saying = "";
8185
+ break;
8186
+ case "tool_call":
8187
+ this.asks.add({
8188
+ callId: text("call_id"),
8189
+ question: text("question"),
8190
+ replaces: ev.replaces_earlier === true,
8191
+ heard: this.heard,
8192
+ utterance: text("message_id") ? { itemId: text("item_id"), messageId: text("message_id") } : null
8193
+ });
8194
+ this.setPhase("thinking");
8195
+ break;
8196
+ case "response.done":
8197
+ if (!this.player?.playing) this.settle();
8198
+ break;
8199
+ case "session.ending":
8200
+ this.endingReason = CLOSE_REASONS[text("reason")] ?? "lost";
8201
+ break;
8202
+ case "session.finish":
8203
+ this.finishAfterGoodbye();
8204
+ break;
8205
+ case "error":
8206
+ console.warn("[voice]", ev.code, ev.message);
8207
+ break;
8208
+ }
8209
+ }
8210
+ /** They are done: end once the goodbye has finished playing. */
8211
+ finishAfterGoodbye() {
8212
+ this.cancelFinish();
8213
+ const deadline = Date.now() + FINISH_MAX_WAIT_MS;
8214
+ const check = () => {
8215
+ if (this.ended) return;
8216
+ if (this.player?.playing && Date.now() < deadline) {
8217
+ this.finishTimer = setTimeout(check, 200);
8218
+ return;
8219
+ }
8220
+ this.finishTimer = setTimeout(() => this.end("finished"), FINISH_GRACE_MS);
8221
+ };
8222
+ check();
8223
+ }
8224
+ cancelFinish() {
8225
+ if (this.finishTimer) clearTimeout(this.finishTimer);
8226
+ this.finishTimer = null;
8227
+ }
8228
+ release() {
8229
+ this.node?.port.close();
8230
+ this.node?.disconnect();
8231
+ this.stream?.getTracks().forEach((t) => t.stop());
8232
+ this.player?.stop();
8233
+ void this.ctx?.close().catch(() => void 0);
8234
+ if (this.blobUrl) URL.revokeObjectURL(this.blobUrl);
8235
+ this.blobUrl = null;
8236
+ this.inputAnalyser = null;
8237
+ this.outputAnalyser = null;
8238
+ this.node = null;
8239
+ this.stream = null;
8240
+ this.ctx = null;
8241
+ }
8242
+ end(reason, detail) {
8243
+ if (this.ended) return;
8244
+ this.ended = true;
8245
+ this.cancelFinish();
8246
+ const ws = this.ws;
8247
+ this.ws = null;
8248
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) ws.close(1e3);
8249
+ this.release();
8250
+ this.h.onEnd(reason, detail);
8251
+ }
8252
+ };
8253
+
8254
+ // src/voice/voiceprint.ts
8255
+ 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("");
8256
+ var STEP_MS = 42;
8257
+ function loudness(rms2) {
8258
+ const v = Math.max(0, rms2 - 4e-3);
8259
+ return Math.min(1, Math.sqrt(v * 7));
8260
+ }
8261
+ function nextColumn(levels, phase, t, seed) {
8262
+ const you = loudness(levels.input);
8263
+ const javier = loudness(levels.output);
8264
+ if (javier > 0.05 && javier >= you * 0.8) return { amp: javier, voice: 2 /* Javier */, seed };
8265
+ if (you > 0.12) return { amp: you, voice: 1 /* You */, seed };
8266
+ if (phase === "thinking" || phase === "connecting") {
8267
+ const swell = 0.24 + 0.17 * Math.sin(t / 170) * Math.sin(t / 530);
8268
+ return { amp: Math.max(0.06, swell), voice: 3 /* Working */, seed };
8269
+ }
8270
+ return { amp: 0.06 + 0.035 * Math.sin(t / 650), voice: 0 /* Quiet */, seed };
8271
+ }
8272
+ function cellIntensity(amp, rows, row) {
8273
+ const half = rows / 2;
8274
+ const d = Math.abs(row + 0.5 - half) / half;
8275
+ const reach = Math.max(amp, 0);
8276
+ if (reach <= 0 || d > reach) return 0;
8277
+ return 0.5 + 0.5 * Math.pow(1 - d / reach, 0.8);
8278
+ }
8279
+ function hash(a, b) {
8280
+ let h = Math.imul(a | 0, 374761393) + Math.imul(b | 0, 668265263) | 0;
8281
+ h = Math.imul(h ^ h >>> 13, 1274126177);
8282
+ return (h ^ h >>> 16) >>> 0;
8283
+ }
8284
+ function glyphFor(seed, row) {
8285
+ return GLYPHS[hash(seed, row) % GLYPHS.length];
8286
+ }
8287
+ function ageFade(x, width) {
8288
+ const span = width * 0.42;
8289
+ if (span <= 0) return 1;
8290
+ const k = Math.min(1, Math.max(0, x / span));
8291
+ return k * k * (3 - 2 * k);
8292
+ }
8293
+ function clock(ms) {
8294
+ const s = Math.max(0, Math.floor(ms / 1e3));
8295
+ return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
8296
+ }
8297
+ function captionTail(text, max) {
8298
+ const t = text.trim();
8299
+ if (t.length <= max) return t;
8300
+ const cut = t.slice(t.length - max);
8301
+ const space = cut.indexOf(" ");
8302
+ return `\u2026${space > 0 && space < 24 ? cut.slice(space + 1) : cut}`;
8303
+ }
8304
+ function parseCssColor(value, fallback) {
8305
+ const v = value.trim();
8306
+ const srgb = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)/i.exec(v);
8307
+ if (srgb) return [srgb[1], srgb[2], srgb[3]].map((n) => Math.round(Math.min(1, Math.max(0, Number(n))) * 255));
8308
+ const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(v);
8309
+ if (rgb) return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])].map((n) => Math.round(n));
8310
+ return fallback;
8311
+ }
8312
+
8313
+ // src/voice/useVoiceSession.ts
8314
+ function useVoiceSession(host, connection) {
8315
+ const [available, setAvailable] = useState(false);
8316
+ const [defaultName, setDefaultName] = useState(null);
8317
+ const [maxSessionSecs, setMaxSessionSecs] = useState(0);
8318
+ const [phase, setPhase] = useState("off");
8319
+ const [heard, setHeard] = useState("");
8320
+ const [saying, setSaying] = useState("");
8321
+ const [ended, setEnded] = useState(null);
8322
+ const [startedAt, setStartedAt] = useState(null);
8323
+ const session = useRef(null);
8324
+ const hostRef = useRef(host);
8325
+ hostRef.current = host;
8326
+ const connectionRef = useRef(connection);
8327
+ connectionRef.current = connection;
8328
+ const { baseUrl } = connection;
8329
+ useEffect(() => {
8330
+ const controller = new AbortController();
8331
+ void getVoiceStatus(connectionRef.current, controller.signal).then((s) => {
8332
+ if (controller.signal.aborted) return;
8333
+ setAvailable(s.enabled);
8334
+ setMaxSessionSecs(s.maxSessionSecs);
8335
+ setDefaultName(s.persona?.name ?? null);
8336
+ });
8337
+ return () => controller.abort();
8338
+ }, [baseUrl]);
8339
+ const start = useCallback(() => {
8340
+ if (session.current) return;
8341
+ setEnded(null);
8342
+ setHeard("");
8343
+ setSaying("");
8344
+ setStartedAt(null);
8345
+ const s = new VoiceSession(
8346
+ {
8347
+ onPhase: (p) => {
8348
+ setPhase(p);
8349
+ if (p !== "connecting") setStartedAt((at) => at ?? Date.now());
8350
+ },
8351
+ onHeard: (text) => setHeard(text),
8352
+ onSaying: (text) => setSaying(text),
8353
+ onAsk: (question, h, utterance, progress, signal) => hostRef.current.onAsk(question, h, utterance, progress, signal),
8354
+ onUtterance: (u) => hostRef.current.onUtterance?.(u),
8355
+ onReply: (r) => hostRef.current.onReply?.(r),
8356
+ onEnd: (reason, detail) => {
8357
+ if (session.current === s) session.current = null;
8358
+ setPhase("off");
8359
+ setStartedAt(null);
8360
+ setEnded(reason === "stopped" || reason === "finished" ? null : { reason, detail });
8361
+ }
8362
+ },
8363
+ { ...connectionRef.current }
8364
+ );
8365
+ session.current = s;
8366
+ void s.start();
8367
+ }, []);
8368
+ const stop = useCallback(() => session.current?.stop(), []);
8369
+ const toggle = useCallback(() => session.current ? stop() : start(), [start, stop]);
8370
+ const dismiss = useCallback(() => setEnded(null), []);
8371
+ const levels = useCallback(() => session.current?.levels() ?? SILENT, []);
8372
+ const level = useCallback(() => {
8373
+ const l = levels();
8374
+ return Math.max(loudness(l.input), loudness(l.output));
8375
+ }, [levels]);
8376
+ useEffect(() => () => session.current?.stop(), []);
8377
+ return {
8378
+ available,
8379
+ defaultName,
8380
+ phase,
8381
+ heard,
8382
+ saying,
8383
+ ended,
8384
+ startedAt,
8385
+ maxSessionSecs,
8386
+ levels,
8387
+ level,
8388
+ buttonState: phase === "off" ? "off" : phase === "connecting" ? "connecting" : "live",
8389
+ start,
8390
+ stop,
8391
+ toggle,
8392
+ dismiss
8393
+ };
8394
+ }
8395
+ function voiceEndedMessage(ended, name = "the assistant") {
8396
+ if (!ended) return null;
8397
+ switch (ended.reason) {
8398
+ case "time_limit":
8399
+ return "Voice ends after a while to keep costs in check. Tap the microphone to keep talking.";
8400
+ case "replaced":
8401
+ return "Voice moved to your other window.";
8402
+ case "blocked":
8403
+ 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.";
8404
+ case "mic_denied":
8405
+ return `Allow microphone access to talk to ${name}.`;
8406
+ case "unavailable":
8407
+ return ended.detail && !/^[a-z_]+$/.test(ended.detail) ? ended.detail : "Voice could not connect. Try again in a moment.";
8408
+ case "lost":
8409
+ return "Voice disconnected. Tap the microphone to reconnect.";
8410
+ default:
8411
+ return null;
8412
+ }
8413
+ }
8414
+
8415
+ // src/voice/styles.ts
8416
+ var VOICE_DECK_CSS = `
8417
+ @property --yf-voice-angle { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
8418
+ @keyframes yf-voice-orbit { to { --yf-voice-angle: 360deg; } }
8419
+ @keyframes yf-voice-rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
8420
+ @keyframes yf-voice-beacon {
8421
+ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--yf-voice-live) 55%, transparent); }
8422
+ 70% { box-shadow: 0 0 0 9px color-mix(in srgb, var(--yf-voice-live) 0%, transparent); }
8423
+ 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--yf-voice-live) 0%, transparent); }
8424
+ }
8425
+ .yf-voice-deck {
8426
+ --yf-voice-live: var(--yf-voice-accent);
8427
+ position: relative;
8428
+ border-radius: 16px;
8429
+ padding: 1px;
8430
+ background:
8431
+ conic-gradient(
8432
+ from var(--yf-voice-angle),
8433
+ transparent 0deg,
8434
+ color-mix(in srgb, var(--yf-voice-live) 95%, transparent) 50deg,
8435
+ color-mix(in srgb, var(--yf-voice-live) 15%, transparent) 110deg,
8436
+ transparent 180deg,
8437
+ transparent 360deg
8438
+ ),
8439
+ linear-gradient(color-mix(in srgb, var(--yf-voice-live) 22%, transparent), color-mix(in srgb, var(--yf-voice-live) 22%, transparent));
8440
+ box-shadow: 0 24px 60px -32px color-mix(in srgb, var(--yf-voice-live) 65%, transparent);
8441
+ animation: yf-voice-orbit 5s linear infinite;
8442
+ transition: box-shadow 0.5s ease;
8443
+ }
8444
+ .yf-voice-deck[data-phase='hearing'] { --yf-voice-live: var(--yf-voice-ink); animation-duration: 2.6s; }
8445
+ .yf-voice-deck[data-phase='thinking'] { animation-duration: 1.4s; }
8446
+ .yf-voice-deck[data-phase='speaking'] { animation-duration: 3.2s; }
8447
+ .yf-voice-deck[data-phase='listening'],
8448
+ .yf-voice-deck[data-phase='connecting'] { animation-duration: 9s; }
8449
+ .yf-voice-deck__panel {
8450
+ position: relative;
8451
+ overflow: hidden;
8452
+ border-radius: 15px;
8453
+ background:
8454
+ radial-gradient(120% 160% at 100% 0%, color-mix(in srgb, var(--yf-voice-live) 13%, transparent), transparent 58%),
8455
+ radial-gradient(90% 120% at 0% 100%, color-mix(in srgb, var(--yf-voice-live) 5%, transparent), transparent 60%),
8456
+ color-mix(in srgb, var(--yf-voice-panel) 90%, transparent);
8457
+ -webkit-backdrop-filter: blur(14px) saturate(1.2);
8458
+ backdrop-filter: blur(14px) saturate(1.2);
8459
+ animation: yf-voice-rise 0.32s cubic-bezier(0.2, 0.7, 0.2, 1) both;
8460
+ }
8461
+ .yf-voice-beacon {
8462
+ width: 8px;
8463
+ height: 8px;
8464
+ flex-shrink: 0;
8465
+ border-radius: 999px;
8466
+ background: var(--yf-voice-live);
8467
+ animation: yf-voice-beacon 1.8s ease-out infinite;
8468
+ transition: background-color 0.3s ease;
8469
+ }
8470
+ .yf-voice-deck[data-phase='listening'] .yf-voice-beacon { animation-duration: 3.2s; }
8471
+ .yf-voice-phase {
8472
+ font-size: 10.5px;
8473
+ font-weight: 600;
8474
+ letter-spacing: 0.14em;
8475
+ text-transform: uppercase;
8476
+ color: var(--yf-voice-live);
8477
+ transition: color 0.3s ease;
8478
+ }
8479
+ .yf-voice-clock { font-size: 11px; font-variant-numeric: tabular-nums; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--yf-voice-muted); }
8480
+ .yf-voice-clock[data-soon] { color: var(--yf-voice-accent); }
8481
+ .yf-voice-end {
8482
+ display: inline-flex;
8483
+ align-items: center;
8484
+ gap: 7px;
8485
+ height: 28px;
8486
+ padding: 0 12px 0 10px;
8487
+ border-radius: 999px;
8488
+ border: 1px solid color-mix(in srgb, var(--yf-voice-ink) 16%, transparent);
8489
+ background: color-mix(in srgb, var(--yf-voice-ink) 5%, transparent);
8490
+ color: color-mix(in srgb, var(--yf-voice-ink) 85%, transparent);
8491
+ font-size: 12px;
8492
+ font-weight: 500;
8493
+ transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
8494
+ -webkit-tap-highlight-color: transparent;
8495
+ }
8496
+ .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); }
8497
+ .yf-voice-end:active { transform: scale(0.97); }
8498
+ .yf-voice-end__mark { width: 8px; height: 8px; border-radius: 2px; background: currentColor; }
8499
+ .yf-voice-print {
8500
+ -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 100%);
8501
+ mask-image: linear-gradient(90deg, transparent 0, #000 12%, #000 100%);
8502
+ }
8503
+ .yf-voice-caption { display: flex; min-width: 0; align-items: baseline; gap: 8px; color: color-mix(in srgb, var(--yf-voice-ink) 72%, transparent); }
8504
+ .yf-voice-caption--javier { display: block; color: var(--yf-voice-ink); font-weight: 450; text-wrap: pretty; }
8505
+ .yf-voice-hint { color: var(--yf-voice-muted); }
8506
+ .yf-voice-who {
8507
+ flex-shrink: 0;
8508
+ font-size: 9.5px;
8509
+ font-weight: 600;
8510
+ letter-spacing: 0.14em;
8511
+ text-transform: uppercase;
8512
+ color: var(--yf-voice-muted);
8513
+ }
8514
+ .yf-voice-deck[data-phone] { border-radius: 20px; }
8515
+ .yf-voice-deck[data-phone] .yf-voice-deck__panel { border-radius: 19px; }
8516
+ .yf-voice-deck[data-phone] .yf-voice-end { height: 40px; padding: 0 16px 0 14px; font-size: 14px; }
8517
+ .yf-voice-deck[data-phone] .yf-voice-phase { font-size: 11.5px; }
8518
+ @media (prefers-reduced-motion: reduce) {
8519
+ .yf-voice-deck, .yf-voice-beacon, .yf-voice-deck__panel { animation: none; }
8520
+ }
8521
+ `;
8522
+ var STYLE_ID = "yf-voice-deck-styles";
8523
+ function ensureVoiceDeckStyles(doc = typeof document === "undefined" ? void 0 : document) {
8524
+ if (!doc || doc.getElementById(STYLE_ID)) return;
8525
+ const style = doc.createElement("style");
8526
+ style.id = STYLE_ID;
8527
+ style.textContent = VOICE_DECK_CSS;
8528
+ doc.head.appendChild(style);
8529
+ }
8530
+ function VoiceDeck({ voice, name, phone = false, hint }) {
8531
+ const t = useTheme();
8532
+ useEffect(() => ensureVoiceDeckStyles(), []);
8533
+ const now = useClock(voice.startedAt !== null);
8534
+ if (voice.phase === "off") return null;
8535
+ const speaker = name ?? voice.defaultName ?? "Assistant";
8536
+ const phaseWords = {
8537
+ connecting: "Connecting",
8538
+ listening: "Listening",
8539
+ hearing: "Hearing you",
8540
+ thinking: "Working on it",
8541
+ speaking: speaker
8542
+ };
8543
+ const { startedAt, maxSessionSecs } = voice;
8544
+ const elapsed = startedAt === null ? 0 : now - startedAt;
8545
+ const remaining = maxSessionSecs > 0 && startedAt !== null ? maxSessionSecs * 1e3 - elapsed : Infinity;
8546
+ const endingSoon = remaining < 6e4;
8547
+ const said = captionTail(voice.saying, phone ? 170 : 240);
8548
+ const words = captionTail(voice.heard, phone ? 90 : 140);
8549
+ const vars = {
8550
+ "--yf-voice-accent": t.primary,
8551
+ "--yf-voice-ink": t.text,
8552
+ "--yf-voice-muted": t.textMuted,
8553
+ "--yf-voice-panel": t.bgRaised
8554
+ };
8555
+ return /* @__PURE__ */ jsxs("div", { className: "yf-voice-deck", "data-phase": voice.phase, "data-phone": phone ? "" : void 0, style: vars, children: [
8556
+ /* @__PURE__ */ jsxs("div", { className: "yf-voice-deck__panel", children: [
8557
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-2.5 ${phone ? "px-4 pt-3.5" : "px-4 pt-3"}`, children: [
8558
+ /* @__PURE__ */ jsx("span", { className: "yf-voice-beacon", "aria-hidden": "true" }),
8559
+ /* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "yf-voice-phase", children: phaseWords[voice.phase] }),
8560
+ startedAt !== null ? /* @__PURE__ */ 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,
8561
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
8562
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: voice.stop, className: "yf-voice-end", "aria-label": "End voice", children: [
8563
+ /* @__PURE__ */ jsx("span", { className: "yf-voice-end__mark", "aria-hidden": "true" }),
8564
+ "End"
8565
+ ] })
8566
+ ] }),
8567
+ /* @__PURE__ */ jsx(Voiceprint, { phase: voice.phase, levels: voice.levels, height: phone ? 104 : 76, cell: phone ? 13 : 11 }),
8568
+ /* @__PURE__ */ 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: [
8569
+ words ? /* @__PURE__ */ jsxs("p", { className: "yf-voice-caption", style: { fontSize: phone ? 13.5 : 12.5 }, children: [
8570
+ /* @__PURE__ */ jsx("span", { className: "yf-voice-who", children: "You" }),
8571
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: words })
8572
+ ] }) : null,
8573
+ said ? /* @__PURE__ */ 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__ */ jsx("p", { className: "yf-voice-hint", style: { fontSize: phone ? 14 : 12.5 }, children: voice.phase === "connecting" ? "Opening the line\u2026" : hint ?? "Ask anything. Talk over me to interrupt." }) : null
8574
+ ] })
8575
+ ] }),
8576
+ /* @__PURE__ */ jsx("span", { "data-voice-probe": "ink", hidden: true, style: { color: "var(--yf-voice-ink)" } }),
8577
+ /* @__PURE__ */ jsx("span", { "data-voice-probe": "accent", hidden: true, style: { color: "var(--yf-voice-accent)" } }),
8578
+ /* @__PURE__ */ jsx("span", { "data-voice-probe": "muted", hidden: true, style: { color: "var(--yf-voice-muted)" } })
8579
+ ] });
8580
+ }
8581
+ function useClock(running) {
8582
+ const [now, setNow] = useState(() => Date.now());
8583
+ useEffect(() => {
8584
+ if (!running) return void 0;
8585
+ setNow(Date.now());
8586
+ const id = window.setInterval(() => setNow(Date.now()), 1e3);
8587
+ return () => window.clearInterval(id);
8588
+ }, [running]);
8589
+ return now;
8590
+ }
8591
+ function Voiceprint({ phase, levels, height, cell }) {
8592
+ const canvasRef = useRef(null);
8593
+ const phaseRef = useRef(phase);
8594
+ phaseRef.current = phase;
8595
+ const levelsRef = useRef(levels);
8596
+ levelsRef.current = levels;
8597
+ useEffect(() => {
8598
+ const canvas = canvasRef.current;
8599
+ const ctx = canvas?.getContext("2d");
8600
+ if (!canvas || !ctx) return void 0;
8601
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
8602
+ const cellW = Math.round(cell * 0.8);
8603
+ const cellH = Math.round(cell * 0.95);
8604
+ let width = 0;
8605
+ let dpr = 1;
8606
+ let rows = 1;
8607
+ let columns = [];
8608
+ let colors = { ink: [236, 232, 225], accent: [255, 140, 66], muted: [140, 140, 140] };
8609
+ let seed = 1;
8610
+ let last = performance.now();
8611
+ let colorsAt = 0;
8612
+ let raf = 0;
8613
+ const readColors = () => {
8614
+ const deck = canvas.closest(".yf-voice-deck");
8615
+ const probe = (which, fallback) => {
8616
+ const el = deck?.querySelector(`[data-voice-probe="${which}"]`);
8617
+ return el ? parseCssColor(getComputedStyle(el).color, fallback) : fallback;
8618
+ };
8619
+ colors = { ink: probe("ink", colors.ink), accent: probe("accent", colors.accent), muted: probe("muted", colors.muted) };
8620
+ };
8621
+ const resize = () => {
8622
+ dpr = Math.min(window.devicePixelRatio || 1, 3);
8623
+ width = canvas.clientWidth;
8624
+ canvas.width = Math.round(width * dpr);
8625
+ canvas.height = Math.round(height * dpr);
8626
+ rows = Math.max(3, Math.floor(height / cellH));
8627
+ const need = Math.ceil(width / cellW) + 3;
8628
+ columns = columns.slice(-need);
8629
+ while (columns.length < need) columns.unshift({ amp: 0.05, voice: 0 /* Quiet */, seed: seed++ });
8630
+ };
8631
+ const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a.toFixed(3)})`;
8632
+ const colorOf = (v) => v === 1 /* You */ ? colors.ink : v === 0 /* Quiet */ ? colors.muted : colors.accent;
8633
+ const draw = (time) => {
8634
+ if (time - colorsAt > 1e3) {
8635
+ readColors();
8636
+ colorsAt = time;
8637
+ }
8638
+ const step = reduced ? STEP_MS * 2.5 : STEP_MS;
8639
+ while (time - last >= step) {
8640
+ columns.shift();
8641
+ columns.push(nextColumn(levelsRef.current(), phaseRef.current, time, seed++));
8642
+ last += step;
8643
+ }
8644
+ if (time - last > step * 4) last = time;
8645
+ const frac = reduced ? 0 : (time - last) / step;
8646
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
8647
+ ctx.clearRect(0, 0, width, height);
8648
+ ctx.font = `500 ${cell}px "Hiragino Kaku Gothic ProN", "Noto Sans JP", ui-monospace, monospace`;
8649
+ ctx.textAlign = "center";
8650
+ ctx.textBaseline = "middle";
8651
+ const n = columns.length;
8652
+ const top = (height - rows * cellH) / 2 + cellH / 2;
8653
+ for (let i = 0; i < n; i += 1) {
8654
+ const col = columns[i];
8655
+ const x = width - (n - 1 - i + frac) * cellW + cellW / 2 - cellW;
8656
+ if (x < -cellW || x > width + cellW) continue;
8657
+ const fade = ageFade(x, width);
8658
+ const fresh = n - 1 - i < 3;
8659
+ const color = colorOf(col.voice);
8660
+ const reach = Math.min(1, col.amp * 1.15);
8661
+ for (let r = 0; r < rows; r += 1) {
8662
+ const k = cellIntensity(reach, rows, r);
8663
+ const s = fresh && !reduced ? col.seed + Math.floor(time / 90) : col.seed;
8664
+ const y = top + r * cellH;
8665
+ if (k <= 0.02) {
8666
+ ctx.fillStyle = rgba(colors.muted, 0.07 * (0.25 + 0.75 * fade));
8667
+ ctx.fillText(glyphFor(s + 7, r), x, y);
8668
+ continue;
8669
+ }
8670
+ const alpha = Math.min(1, k * (col.voice === 0 /* Quiet */ ? 0.45 : 0.95) * (0.12 + 0.88 * fade) * (fresh ? 1.3 : 1));
8671
+ const bright = k > 0.8 && col.voice !== 0 /* Quiet */ && !reduced;
8672
+ if (bright) {
8673
+ ctx.shadowColor = rgba(color, 0.9);
8674
+ ctx.shadowBlur = 7;
8675
+ }
8676
+ ctx.fillStyle = rgba(color, alpha);
8677
+ ctx.fillText(glyphFor(s, r), x, y);
8678
+ if (bright) ctx.shadowBlur = 0;
8679
+ }
8680
+ }
8681
+ const head = columns[n - 1];
8682
+ if (head) {
8683
+ const c = colorOf(head.voice);
8684
+ const radius = height * 0.48;
8685
+ ctx.save();
8686
+ ctx.translate(width, height / 2);
8687
+ ctx.scale(1.9, 1);
8688
+ const glow = ctx.createRadialGradient(0, 0, 0, 0, 0, radius);
8689
+ glow.addColorStop(0, rgba(c, 0.1 + 0.3 * head.amp));
8690
+ glow.addColorStop(1, rgba(c, 0));
8691
+ ctx.fillStyle = glow;
8692
+ ctx.beginPath();
8693
+ ctx.arc(0, 0, radius, 0, Math.PI * 2);
8694
+ ctx.fill();
8695
+ ctx.restore();
8696
+ const reach = Math.max(6, Math.min(1, head.amp * 1.15) * height * 0.9);
8697
+ const line = ctx.createLinearGradient(0, (height - reach) / 2, 0, (height + reach) / 2);
8698
+ line.addColorStop(0, rgba(c, 0));
8699
+ line.addColorStop(0.5, rgba(c, 0.5 + 0.5 * head.amp));
8700
+ line.addColorStop(1, rgba(c, 0));
8701
+ ctx.fillStyle = line;
8702
+ ctx.fillRect(width - 1.5, (height - reach) / 2, 1.5, reach);
8703
+ }
8704
+ raf = window.requestAnimationFrame(draw);
8705
+ };
8706
+ readColors();
8707
+ resize();
8708
+ const observer = new ResizeObserver(resize);
8709
+ observer.observe(canvas);
8710
+ raf = window.requestAnimationFrame(draw);
8711
+ return () => {
8712
+ window.cancelAnimationFrame(raf);
8713
+ observer.disconnect();
8714
+ };
8715
+ }, [height, cell]);
8716
+ return /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: "yf-voice-print block w-full", style: { height }, "aria-hidden": "true" });
8717
+ }
7616
8718
 
7617
8719
  // src/knowledge/graphTheme.ts
7618
8720
  var NODE_TYPE_COLORS = {
@@ -14646,4 +15748,4 @@ var terminalClassNames = {
14646
15748
  promptSymbol: "terminal-prompt-symbol"
14647
15749
  };
14648
15750
 
14649
- export { ADVANCE_PHASE_SIGNAL, AGENT_PALETTE, AgentAvatar, AgentPresenceBar, AgentStepTrace, AnnotationThread, CLAIM_STATUS_COLORS, ChatTerminal, CheckpointCard, ClaimNode, ConstructionProgress, CoreTerminal, DEFAULT_AGENT_LABEL, DOCUMENT_PIPELINE_STEPS, DocumentProcessingProgress, EDGE_TYPE_COLORS, EdgeDetailDrawer, EdgeMarkerDefs, CoreTerminal as EmbeddedTerminal, ExpandableTerminal, GraphChatPanel, GraphEdge, GraphNode, InlineTerminal, KnowledgeGraphView, LIVE_KG_ID, LiveActivityTicker, MarkdownContent, MarkdownEditor, NODE_TYPE_COLORS, NODE_TYPE_LABELS, NodeDetailDrawer, PROGRESS_EVENT_NAME, PipelineProgressBar, ReasoningHistoryCard, ReasoningOverlay, ReasoningView, SSE_KEEPALIVE_MS, SSE_STALL_MS, SYNTHESIS_DONE_SIGNAL, ScopedReasoningProvider, SettlementBar, SlashMentionPicker, StageCanvas, StandardTerminal, TerminalInputBar, TerminalThemeProvider, agentSeed, agentStepTraceExtra, avatarColor, blueScope, buildClaimsGraph, buildTerminalCommand, buttonShadow, captureReasoningSnapshot, defaultTheme, dispatchProgressEvent, documentEventToActivity, emeraldCollab, emeraldRevised, eventToActivity, extractKGSources, extractKgId, filterCommands, formatAssetDisplay, formatPaymentAmount, formatTokenAssetId, getAgentColor, getBuiltInCommands, getClaimStatusColors, getEdgeColor, getEdgeTypeLabel, getNodeColors, getNodeTypeLabel, getVocabTypeColors, getVocabTypeLabel, greenAction, handleSSEStream, handleStreamingResponse, hostApp, initials, isFixedKgId, isNaturalLanguage, layoutGraph, mergeTheme, openPipelineEventStream, orangeRebuttal, parseTerminalCommand, pipelineBg, processStreamLines, purple, reduceProgress, renderMarkdownBlock, resolvePaymentAssetDisplay, snapshotToSubgraph, terminalClassNames, terminalColors, useScopedProgressEvent, useSlashMentionPicker, useTheme, warmBg, withStallTimeout };
15751
+ export { ADVANCE_PHASE_SIGNAL, AGENT_PALETTE, AgentAvatar, AgentPresenceBar, AgentStepTrace, AnnotationThread, AskQueue, CLAIM_STATUS_COLORS, ChatTerminal, CheckpointCard, ClaimNode, ConstructionProgress, CoreTerminal, DEFAULT_AGENT_LABEL, DOCUMENT_PIPELINE_STEPS, DocumentProcessingProgress, EDGE_TYPE_COLORS, EdgeDetailDrawer, EdgeMarkerDefs, CoreTerminal as EmbeddedTerminal, ExpandableTerminal, GraphChatPanel, GraphEdge, GraphNode, InlineTerminal, KnowledgeGraphView, LIVE_KG_ID, LiveActivityTicker, MarkdownContent, MarkdownEditor, NODE_TYPE_COLORS, NODE_TYPE_LABELS, NodeDetailDrawer, PROGRESS_EVENT_NAME, PipelineProgressBar, REPLACED_OUTPUT, ReasoningHistoryCard, ReasoningOverlay, ReasoningView, SILENT, SSE_KEEPALIVE_MS, SSE_STALL_MS, SYNTHESIS_DONE_SIGNAL, ScopedReasoningProvider, SettlementBar, SlashMentionPicker, StageCanvas, StandardTerminal, TerminalInputBar, TerminalThemeProvider, VOICE_CAPTURE_WORKLET_SOURCE, VOICE_LANGUAGES, VOICE_NAMES, VoiceDeck, VoiceInputButton, VoiceSession, agentSeed, agentStepTraceExtra, avatarColor, blueScope, buildClaimsGraph, buildTerminalCommand, buttonShadow, captureReasoningSnapshot, defaultTheme, dispatchProgressEvent, documentEventToActivity, emeraldCollab, emeraldRevised, encodePersona, eventToActivity, extractKGSources, extractKgId, filterCommands, formatAssetDisplay, formatPaymentAmount, formatTokenAssetId, getAgentColor, getBuiltInCommands, getClaimStatusColors, getEdgeColor, getEdgeTypeLabel, getNodeColors, getNodeTypeLabel, getVocabTypeColors, getVocabTypeLabel, getVoiceStatus, greenAction, handleSSEStream, handleStreamingResponse, hostApp, initials, isFixedKgId, isNaturalLanguage, layoutGraph, mergeTheme, openPipelineEventStream, orangeRebuttal, parseTerminalCommand, pipelineBg, processStreamLines, purple, reduceProgress, renderMarkdownBlock, resolvePaymentAssetDisplay, snapshotToSubgraph, socketBase, terminalClassNames, terminalColors, useScopedProgressEvent, useSlashMentionPicker, useTheme, useVoiceSession, voiceEndedMessage, voiceSocketUrl, warmBg, withStallTimeout };