fix: standardize Spanish user-facing copy
This commit is contained in:
@@ -244,7 +244,7 @@ a.inline_marker { display:inline; margin-left:1px; color:#075c77; font-size:.9em
|
|||||||
<h3 id="11-4-data-and-retention" data-cid="kuKGIi">11.4 Data and retention</h3><p data-cid="k_4NLm">Define a matrix by data class:</p><ul data-cid="3v1WWF"><li data-cid="5rJmr1">raw audio;</li><li data-cid="VkSLyu">voice references;</li><li data-cid="8aOIsd">transcripts;</li><li data-cid="J7Y74H">prompt/context;</li><li data-cid="6CPODX">model responses;</li><li data-cid="2gYs6P">tool arguments and results;</li><li data-cid="phF27i">telemetry and auditing.</li></ul><p data-cid="aF8xcz">For each class, approve purpose, legal basis, location, encryption, retention, deletion, export, access, training use and redaction. <strong data-cid="5qKyXe">Pending:</strong> the requester did not supply corporate policies or jurisdictions; this document therefore sets no retention periods.</p>
|
<h3 id="11-4-data-and-retention" data-cid="kuKGIi">11.4 Data and retention</h3><p data-cid="k_4NLm">Define a matrix by data class:</p><ul data-cid="3v1WWF"><li data-cid="5rJmr1">raw audio;</li><li data-cid="VkSLyu">voice references;</li><li data-cid="8aOIsd">transcripts;</li><li data-cid="J7Y74H">prompt/context;</li><li data-cid="6CPODX">model responses;</li><li data-cid="2gYs6P">tool arguments and results;</li><li data-cid="phF27i">telemetry and auditing.</li></ul><p data-cid="aF8xcz">For each class, approve purpose, legal basis, location, encryption, retention, deletion, export, access, training use and redaction. <strong data-cid="5qKyXe">Pending:</strong> the requester did not supply corporate policies or jurisdictions; this document therefore sets no retention periods.</p>
|
||||||
<h2 class="chapter" id="12-observability-and-initial-slos" data-cid="tCwZyI">12. Observability and initial SLOs</h2><h3 id="12-1-telemetry-model" data-cid="m3zjXZ">12.1 Telemetry model</h3><p data-cid="j-u4rg"><strong data-cid="1UhJRp">Traces:</strong> one trace per turn with spans for capture, relay, STT, endpointing, LLM, policy, MCP, TTS, network and playout. The future RC proposes standardized names for propagating W3C Trace Context in <code data-cid="9z3Qpe">_meta</code>, but this form remains a candidate until ratification <a class="inline_marker" href="#ref4" data-cid="wS4Po1">4</a>. In the stable adapter, Vocero will propagate <code data-cid="93rwTu">traceparent</code> through compatible internal metadata without requiring future domain changes.</p><p data-cid="J750jH"><strong data-cid="BBbmRc">Experience metrics</strong></p><ul data-cid="2Ey_Wc"><li data-cid="5o-A_q"><code data-cid="hwNkdv">turn_endpointing_ms</code>: acoustic endpoint to turn commit.</li><li data-cid="zAukNl"><code data-cid="6rWpQr">stt_partial_ms</code> and <code data-cid="ncoQUH">stt_final_ms</code>.</li><li data-cid="YaedG8"><code data-cid="WJGF8k">llm_ttft_ms</code>: commit to first token.</li><li data-cid="NcE_iK"><code data-cid="T1Lm8L">tts_ttfa_ms</code>: available text to first audio.</li><li data-cid="vmcq1R"><code data-cid="AdCatj">ttfs_ms</code>: end of speech to first playable sound.</li><li data-cid="2gr-6y"><code data-cid="tCPnFi">barge_in_stop_ms</code>: interruption detection to effective silence.</li><li data-cid="A3Y_lr"><code data-cid="5iwjTj">tool_latency_ms</code> by tool and outcome.</li><li data-cid="AAhZAj"><code data-cid="aETFuO">turn_e2e_ms</code> by direct or tool-assisted path.</li><li data-cid="7kZ6vC">reconnection, cancellation, false barge-in and abandonment rates.</li></ul><p data-cid="M1dVZO">TTFT and end-to-end latency are recognized as distinct, relevant dimensions for voice/realtime workloads <a class="inline_marker" href="#ref9" data-cid="XoMUg9">9</a>.</p><p data-cid="tFQY1A"><strong data-cid="nbmcXA">Quality metrics</strong></p><ul data-cid="G_hour"><li data-cid="Ns_Vtp">WER/CER by language, accent, noise and domain.</li><li data-cid="5krVT8">early/late endpointing rate.</li><li data-cid="QIKGrD">intelligibility and human assessment of naturalness.</li><li data-cid="VbZp2z">speaker similarity and voice stability.</li><li data-cid="erBq7M">accuracy of tool selection, arguments, policy and task completion.</li></ul><p data-cid="GBKg7X"><strong data-cid="nP3Trf">Resource metrics</strong></p><ul data-cid="Pp9yP4"><li data-cid="9QftJu">GPU utilization and memory; KV cache; per-model queues; tokens/s; audio seconds/s.</li><li data-cid="ydnFOY">buffer depth and age; drops; retransmissions; jitter; playout underruns.</li><li data-cid="xe-hyA">active connections, admitted/rejected sessions and dependency errors.</li></ul>
|
<h2 class="chapter" id="12-observability-and-initial-slos" data-cid="tCwZyI">12. Observability and initial SLOs</h2><h3 id="12-1-telemetry-model" data-cid="m3zjXZ">12.1 Telemetry model</h3><p data-cid="j-u4rg"><strong data-cid="1UhJRp">Traces:</strong> one trace per turn with spans for capture, relay, STT, endpointing, LLM, policy, MCP, TTS, network and playout. The future RC proposes standardized names for propagating W3C Trace Context in <code data-cid="9z3Qpe">_meta</code>, but this form remains a candidate until ratification <a class="inline_marker" href="#ref4" data-cid="wS4Po1">4</a>. In the stable adapter, Vocero will propagate <code data-cid="93rwTu">traceparent</code> through compatible internal metadata without requiring future domain changes.</p><p data-cid="J750jH"><strong data-cid="BBbmRc">Experience metrics</strong></p><ul data-cid="2Ey_Wc"><li data-cid="5o-A_q"><code data-cid="hwNkdv">turn_endpointing_ms</code>: acoustic endpoint to turn commit.</li><li data-cid="zAukNl"><code data-cid="6rWpQr">stt_partial_ms</code> and <code data-cid="ncoQUH">stt_final_ms</code>.</li><li data-cid="YaedG8"><code data-cid="WJGF8k">llm_ttft_ms</code>: commit to first token.</li><li data-cid="NcE_iK"><code data-cid="T1Lm8L">tts_ttfa_ms</code>: available text to first audio.</li><li data-cid="vmcq1R"><code data-cid="AdCatj">ttfs_ms</code>: end of speech to first playable sound.</li><li data-cid="2gr-6y"><code data-cid="tCPnFi">barge_in_stop_ms</code>: interruption detection to effective silence.</li><li data-cid="A3Y_lr"><code data-cid="5iwjTj">tool_latency_ms</code> by tool and outcome.</li><li data-cid="AAhZAj"><code data-cid="aETFuO">turn_e2e_ms</code> by direct or tool-assisted path.</li><li data-cid="7kZ6vC">reconnection, cancellation, false barge-in and abandonment rates.</li></ul><p data-cid="M1dVZO">TTFT and end-to-end latency are recognized as distinct, relevant dimensions for voice/realtime workloads <a class="inline_marker" href="#ref9" data-cid="XoMUg9">9</a>.</p><p data-cid="tFQY1A"><strong data-cid="nbmcXA">Quality metrics</strong></p><ul data-cid="G_hour"><li data-cid="Ns_Vtp">WER/CER by language, accent, noise and domain.</li><li data-cid="5krVT8">early/late endpointing rate.</li><li data-cid="QIKGrD">intelligibility and human assessment of naturalness.</li><li data-cid="VbZp2z">speaker similarity and voice stability.</li><li data-cid="erBq7M">accuracy of tool selection, arguments, policy and task completion.</li></ul><p data-cid="GBKg7X"><strong data-cid="nP3Trf">Resource metrics</strong></p><ul data-cid="Pp9yP4"><li data-cid="9QftJu">GPU utilization and memory; KV cache; per-model queues; tokens/s; audio seconds/s.</li><li data-cid="ydnFOY">buffer depth and age; drops; retransmissions; jitter; playout underruns.</li><li data-cid="xe-hyA">active connections, admitted/rejected sessions and dependency errors.</li></ul>
|
||||||
<h3 id="12-2-initial-targets-not-observed-performance" data-cid="o1IqPd">12.2 Initial targets — not observed performance</h3><blockquote data-cid="ewkw_Y"><p data-cid="MtiLnU"><strong data-cid="6q8cxx">[REC/BENCH]</strong> These values are <strong data-cid="Aa5TVw">provisional engineering targets</strong>, not measured figures, guarantees or a contractual SLO. They must be ratified after benchmarking with real audio, networks, tools and concurrency.</p></blockquote>
|
<h3 id="12-2-initial-targets-not-observed-performance" data-cid="o1IqPd">12.2 Initial targets — not observed performance</h3><blockquote data-cid="ewkw_Y"><p data-cid="MtiLnU"><strong data-cid="6q8cxx">[REC/BENCH]</strong> These values are <strong data-cid="Aa5TVw">provisional engineering targets</strong>, not measured figures, guarantees or a contractual SLO. They must be ratified after benchmarking with real audio, networks, tools and concurrency.</p></blockquote>
|
||||||
<div class="sk-doc-native-table-frame" data-scroll-left-active="false" data-scroll-right-active="false" data-scrolling="false" data-cid="UewefS"><table data-cid="3Lkxis"><thead data-cid="SWgBg1"><tr data-cid="YX23Pt"><th data-cid="fLV9-Q">Indicator</th><th data-cid="PdPaBM">Proposed initial target</th><th data-cid="x-RkYj">Window/percentile</th><th data-cid="L7jYUr">Ratification condition</th></tr></thead><tbody data-cid="SGn-1f"><tr data-cid="ISg1qm"><td data-cid="FSnIs_">Conversational service availability</td><td data-cid="DXM-N-">≥ 99,5 %</td><td data-cid="mP0Cbp">Monthly</td><td data-cid="VXfMex">Exclude agreed windows and define the GPU dependency</td></tr><tr data-cid="QIW15t"><td data-cid="cSvYGf">Response audio start <code data-cid="OkiyqU">ttfs_ms</code></td><td data-cid="l71z6T">≤ 1,200 ms</td><td data-cid="YEYtBX">p95, direct turn</td><td data-cid="AfnwPM">Target corporate network and warm session</td></tr><tr data-cid="ASRmra"><td data-cid="Q56rj9">Barge-in stopping</td><td data-cid="cxb2To">≤ 300 ms</td><td data-cid="qldv4J">p95</td><td data-cid="SnU8xd">Measured through playout silence, not only ACK</td></tr><tr data-cid="xq02_4"><td data-cid="0I_Qns">Turns without platform errors</td><td data-cid="H_4-3I">≥ 99,0 %</td><td data-cid="sm91bg">Monthly</td><td data-cid="IkJZS1">Separate dependency errors from invalid input</td></tr><tr data-cid="ntZaDX"><td data-cid="an4iVp">MCP read executions accepted by the gateway</td><td data-cid="svmQf9">≥ 99,0 %</td><td data-cid="doBQAs">Monthly</td><td data-cid="nlo5iR">Exclude policy rejections and downstream timeouts</td></tr><tr data-cid="u02olk"><td data-cid="aQBhCN">Loss of accepted control events</td><td data-cid="CAW-uo">0</td><td data-cid="SE8KqI">Every window</td><td data-cid="fQ1UeX">Verified by sequence and ACK</td></tr><tr data-cid="13mkhq"><td data-cid="Ze0MIK">Unauthorized voice use</td><td data-cid="CiiGPZ">0</td><td data-cid="huwWIH">Every window</td><td data-cid="DcBt-f">Mandatory alert and audit</td></tr></tbody></table></div><p data-cid="RxZGbL">Provisional budget for <code data-cid="NISkWj">ttfs_ms</code> for analysis, not a commitment:</p><pre class="code-block keep-together" data-cid="LJ82ku"><code class="code language-text" data-cid="qKu-h-">endpointing + STT final + scheduling + LLM TTFT + TTS primer audio + transporte/playout</code></pre><p data-cid="gGyyys">No fixed milliseconds are allocated per stage before benchmarking; the dashboard must show contributions and percentiles separately.</p>
|
<div class="sk-doc-native-table-frame" data-scroll-left-active="false" data-scroll-right-active="false" data-scrolling="false" data-cid="UewefS"><table data-cid="3Lkxis"><thead data-cid="SWgBg1"><tr data-cid="YX23Pt"><th data-cid="fLV9-Q">Indicator</th><th data-cid="PdPaBM">Proposed initial target</th><th data-cid="x-RkYj">Window/percentile</th><th data-cid="L7jYUr">Ratification condition</th></tr></thead><tbody data-cid="SGn-1f"><tr data-cid="ISg1qm"><td data-cid="FSnIs_">Conversational service availability</td><td data-cid="DXM-N-">≥ 99.5 %</td><td data-cid="mP0Cbp">Monthly</td><td data-cid="VXfMex">Exclude agreed windows and define the GPU dependency</td></tr><tr data-cid="QIW15t"><td data-cid="cSvYGf">Response audio start <code data-cid="OkiyqU">ttfs_ms</code></td><td data-cid="l71z6T">≤ 1,200 ms</td><td data-cid="YEYtBX">p95, direct turn</td><td data-cid="AfnwPM">Target corporate network and warm session</td></tr><tr data-cid="ASRmra"><td data-cid="Q56rj9">Barge-in stopping</td><td data-cid="cxb2To">≤ 300 ms</td><td data-cid="qldv4J">p95</td><td data-cid="SnU8xd">Measured through playout silence, not only ACK</td></tr><tr data-cid="xq02_4"><td data-cid="0I_Qns">Turns without platform errors</td><td data-cid="H_4-3I">≥ 99.0 %</td><td data-cid="sm91bg">Monthly</td><td data-cid="IkJZS1">Separate dependency errors from invalid input</td></tr><tr data-cid="ntZaDX"><td data-cid="an4iVp">MCP read executions accepted by the gateway</td><td data-cid="svmQf9">≥ 99.0 %</td><td data-cid="doBQAs">Monthly</td><td data-cid="nlo5iR">Exclude policy rejections and downstream timeouts</td></tr><tr data-cid="u02olk"><td data-cid="aQBhCN">Loss of accepted control events</td><td data-cid="CAW-uo">0</td><td data-cid="SE8KqI">Every window</td><td data-cid="fQ1UeX">Verified by sequence and ACK</td></tr><tr data-cid="13mkhq"><td data-cid="Ze0MIK">Unauthorized voice use</td><td data-cid="CiiGPZ">0</td><td data-cid="huwWIH">Every window</td><td data-cid="DcBt-f">Mandatory alert and audit</td></tr></tbody></table></div><p data-cid="RxZGbL">Provisional budget for <code data-cid="NISkWj">ttfs_ms</code> for analysis, not a commitment:</p><pre class="code-block keep-together" data-cid="LJ82ku"><code class="code language-text" data-cid="qKu-h-">endpointing + STT final + scheduling + LLM TTFT + TTS first audio + transport/playout</code></pre><p data-cid="gGyyys">No fixed milliseconds are allocated per stage before benchmarking; the dashboard must show contributions and percentiles separately.</p>
|
||||||
<h3 id="12-3-initial-alerts" data-cid="VSjV0I">12.3 Initial alerts</h3><ul data-cid="k6nCPX"><li data-cid="bR2-tx">Availability and successful-turn burn rates.</li><li data-cid="NRzADW">Sustained queue growth or buffer age.</li><li data-cid="UxO0GD">GPU OOM, model restart or degraded health.</li><li data-cid="3tEvV4">Divergence between <code data-cid="qazQ-Y">generation.cancel</code> and the last emitted chunk.</li><li data-cid="Qb28W0">MCP error/latency by tool, tenant and dependency.</li><li data-cid="NQMtl-">Denied access or voice anomalies.</li><li data-cid="lSA2BL">Missing traces or uncontrolled cardinality.</li></ul>
|
<h3 id="12-3-initial-alerts" data-cid="VSjV0I">12.3 Initial alerts</h3><ul data-cid="k6nCPX"><li data-cid="bR2-tx">Availability and successful-turn burn rates.</li><li data-cid="NRzADW">Sustained queue growth or buffer age.</li><li data-cid="UxO0GD">GPU OOM, model restart or degraded health.</li><li data-cid="3tEvV4">Divergence between <code data-cid="qazQ-Y">generation.cancel</code> and the last emitted chunk.</li><li data-cid="Qb28W0">MCP error/latency by tool, tenant and dependency.</li><li data-cid="NQMtl-">Denied access or voice anomalies.</li><li data-cid="lSA2BL">Missing traces or uncontrolled cardinality.</li></ul>
|
||||||
<h2 class="chapter" id="13-scalability-backpressure-and-resilience" data-cid="S-LogK">13. Scalability, backpressure and resilience</h2><h3 id="13-1-scaling" data-cid="Tx0fGk">13.1 Scaling</h3><ul data-cid="4QpQzq"><li data-cid="-_zxab"><strong data-cid="vM8-7h">Edge/Relay:</strong> horizontal scaling by connections; minimal state; drain before deployment.</li><li data-cid="10o9TO"><strong data-cid="qYAfma">Orchestrator:</strong> horizontal scaling by session with a store lease; optional affinity as an optimization rather than a correctness requirement.</li><li data-cid="gxrdWw"><strong data-cid="RXXb7w">MCP Gateway:</strong> horizontal scaling; per-downstream pools; per-tenant/tool bulkheads.</li><li data-cid="zHEk8m"><strong data-cid="OJwAHO">Inference:</strong> admission control based on actual capacity, not only CPU; separate STT, LLM and TTS queues; prioritize cancellation and active audio.</li><li data-cid="G5cthX"><strong data-cid="Ye4Twn">Voice cache:</strong> encrypted cache bounded by approved voices; invalidate on version changes/revocation.</li></ul>
|
<h2 class="chapter" id="13-scalability-backpressure-and-resilience" data-cid="S-LogK">13. Scalability, backpressure and resilience</h2><h3 id="13-1-scaling" data-cid="Tx0fGk">13.1 Scaling</h3><ul data-cid="4QpQzq"><li data-cid="-_zxab"><strong data-cid="vM8-7h">Edge/Relay:</strong> horizontal scaling by connections; minimal state; drain before deployment.</li><li data-cid="10o9TO"><strong data-cid="qYAfma">Orchestrator:</strong> horizontal scaling by session with a store lease; optional affinity as an optimization rather than a correctness requirement.</li><li data-cid="gxrdWw"><strong data-cid="RXXb7w">MCP Gateway:</strong> horizontal scaling; per-downstream pools; per-tenant/tool bulkheads.</li><li data-cid="zHEk8m"><strong data-cid="OJwAHO">Inference:</strong> admission control based on actual capacity, not only CPU; separate STT, LLM and TTS queues; prioritize cancellation and active audio.</li><li data-cid="G5cthX"><strong data-cid="Ye4Twn">Voice cache:</strong> encrypted cache bounded by approved voices; invalidate on version changes/revocation.</li></ul>
|
||||||
<h3 id="13-2-backpressure" data-cid="0JvDeL">13.2 Backpressure</h3><p data-cid="utahvp"><strong data-cid="cHawg8">[REC]</strong> Each link publishes <code data-cid="N2slfm">high_watermark</code>, <code data-cid="ke9ISa">low_watermark</code> and an overflow policy. The LLM can produce text faster than TTS or the network can consume it; the system must throttle tokens, group pronounceable units and limit buffered audio. Realtime pipelines document the general problem of fast producers and slow consumers <a class="inline_marker" href="#ref10" data-cid="TffY26">10</a>.</p><p data-cid="PAiaHT">Policies:</p><ol data-cid="34cFBN"><li data-cid="BS0Gyn">Control events never share a blocking queue with audio.</li><li data-cid="9Hbjz9">Stale partial STT transcripts may be coalesced; final transcripts and commits may not.</li><li data-cid="U030et">Discard unsynthesized tokens from a canceled generation.</li><li data-cid="_oDMes">Discard generated but unplayed audio on interruption.</li><li data-cid="gOQW7L">A slow connection cannot consume unbounded memory.</li><li data-cid="zcEc1G">The client reports <code data-cid="upJNXd">buffered_ms</code> and playout; the server adjusts chunking or pauses.</li><li data-cid="D4VlSi">Queues have deadlines; expired work is not processed merely for completeness.</li></ol>
|
<h3 id="13-2-backpressure" data-cid="0JvDeL">13.2 Backpressure</h3><p data-cid="utahvp"><strong data-cid="cHawg8">[REC]</strong> Each link publishes <code data-cid="N2slfm">high_watermark</code>, <code data-cid="ke9ISa">low_watermark</code> and an overflow policy. The LLM can produce text faster than TTS or the network can consume it; the system must throttle tokens, group pronounceable units and limit buffered audio. Realtime pipelines document the general problem of fast producers and slow consumers <a class="inline_marker" href="#ref10" data-cid="TffY26">10</a>.</p><p data-cid="PAiaHT">Policies:</p><ol data-cid="34cFBN"><li data-cid="BS0Gyn">Control events never share a blocking queue with audio.</li><li data-cid="9Hbjz9">Stale partial STT transcripts may be coalesced; final transcripts and commits may not.</li><li data-cid="U030et">Discard unsynthesized tokens from a canceled generation.</li><li data-cid="_oDMes">Discard generated but unplayed audio on interruption.</li><li data-cid="gOQW7L">A slow connection cannot consume unbounded memory.</li><li data-cid="zcEc1G">The client reports <code data-cid="upJNXd">buffered_ms</code> and playout; the server adjusts chunking or pauses.</li><li data-cid="D4VlSi">Queues have deadlines; expired work is not processed merely for completeness.</li></ol>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ the implementation at the recorded date; current operating instructions live in
|
|||||||
and new English internal tool-result wrappers.
|
and new English internal tool-result wrappers.
|
||||||
- Renamed the reasoning engine's log-only acknowledgment field to `received`
|
- Renamed the reasoning engine's log-only acknowledgment field to `received`
|
||||||
and its debug phase to `log_only_phase1`.
|
and its debug phase to `log_only_phase1`.
|
||||||
|
- Standardized browser labels, accessibility text, account/profile dialogs,
|
||||||
|
session feedback, and built-in API errors in Spanish. The interface declares
|
||||||
|
`lang="es"`; internal instructions and diagnostics remain English.
|
||||||
|
|
||||||
## 2026-09-25
|
## 2026-09-25
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -177,7 +177,7 @@ def user_view(request) -> dict:
|
|||||||
token = _field(info, "access_token")
|
token = _field(info, "access_token")
|
||||||
out = {
|
out = {
|
||||||
"loggedIn": True,
|
"loggedIn": True,
|
||||||
"username": _field(user, "preferred_username") or _field(user, "name") or "you",
|
"username": _field(user, "preferred_username") or _field(user, "name") or "tú",
|
||||||
"avatar": _field(user, "picture"),
|
"avatar": _field(user, "picture"),
|
||||||
"tier": resolve_tier(user, token),
|
"tier": resolve_tier(user, token),
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-69
@@ -1,12 +1,12 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate, max-age=0" />
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate, max-age=0" />
|
||||||
<meta http-equiv="Pragma" content="no-cache" />
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
<meta http-equiv="Expires" content="0" />
|
<meta http-equiv="Expires" content="0" />
|
||||||
<title>Minimal Conversation · S2S backend (WebSocket)</title>
|
<title>Vocero · Conversación de voz</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
topbar control cluster. Clicking slides in the offcanvas sidebar on
|
topbar control cluster. Clicking slides in the offcanvas sidebar on
|
||||||
mobile; it's a no-op hover hint on desktop where sidebar is always
|
mobile; it's a no-op hover hint on desktop where sidebar is always
|
||||||
shown. -->
|
shown. -->
|
||||||
<button id="reports-sidebar-toggle" class="icon-btn" title="Reports programados" aria-label="Abrir panel de reportes" aria-expanded="false" aria-controls="reports-sidebar" hidden data-reports-sidebar-toggle data-debug-only="true" data-theme-only="dark">
|
<button id="reports-sidebar-toggle" class="icon-btn" title="Reportes programados" aria-label="Abrir panel de reportes" aria-expanded="false" aria-controls="reports-sidebar" hidden data-reports-sidebar-toggle data-debug-only="true" data-theme-only="dark">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h18v18H3z" fill="none"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h18v18H3z" fill="none"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
|
||||||
<span id="reports-sidebar-badge" class="chat-badge" aria-hidden="true"></span>
|
<span id="reports-sidebar-badge" class="chat-badge" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -160,22 +160,22 @@
|
|||||||
<!-- HF login chip / sign-in pill. Populated by ui/account.js; only
|
<!-- HF login chip / sign-in pill. Populated by ui/account.js; only
|
||||||
shown when the deploy runs behind a load balancer with OAuth. -->
|
shown when the deploy runs behind a load balancer with OAuth. -->
|
||||||
<div id="account" class="account" hidden></div>
|
<div id="account" class="account" hidden></div>
|
||||||
<button id="about-btn-m" class="icon-btn about-btn-mobile" title="About this space" aria-label="About this space">
|
<button id="about-btn-m" class="icon-btn about-btn-mobile" title="Acerca de este espacio" aria-label="Acerca de este espacio">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="tools-btn" class="icon-btn" title="Tools" aria-label="Tools">
|
<button id="tools-btn" class="icon-btn" title="Herramientas" aria-label="Herramientas">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="rag-btn" class="icon-btn" title="Custom RAG context" aria-label="Custom RAG context">
|
<button id="rag-btn" class="icon-btn" title="Contexto RAG personalizado" aria-label="Contexto RAG personalizado">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="settings-btn" class="icon-btn" title="Settings" aria-label="Settings">
|
<button id="settings-btn" class="icon-btn" title="Configuración" aria-label="Configuración">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .66.39 1.25 1 1.51H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c0 .66.39 1.25 1 1.51H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<a class="feedback-link" href="https://tally.so/r/pb8LWq" target="_blank" rel="noopener noreferrer">Enviar feedback</a>
|
<a class="feedback-link" href="https://tally.so/r/pb8LWq" target="_blank" rel="noopener noreferrer">Enviar comentarios</a>
|
||||||
|
|
||||||
<!-- Theme=white only: Status card between topbar and stage
|
<!-- Theme=white only: Status card between topbar and stage
|
||||||
Ready to converse / Reports, analysis, data and more.
|
Ready to converse / Reports, analysis, data and more.
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
<path id="mga-hit" class="mga-hit" fill="none" />
|
<path id="mga-hit" class="mga-hit" fill="none" />
|
||||||
<circle id="mga-handle" class="mga-handle" r="3" />
|
<circle id="mga-handle" class="mga-handle" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
<button id="mic-btn" class="side-btn" type="button" aria-label="Mute" title="Mute" aria-hidden="true">
|
<button id="mic-btn" class="side-btn" type="button" aria-label="Silenciar micrófono" title="Silenciar micrófono" aria-hidden="true">
|
||||||
<svg class="mic-on" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/></svg>
|
<svg class="mic-on" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><line x1="12" y1="19" x2="12" y2="22"/></svg>
|
||||||
<svg class="mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="2" x2="22" y2="22"/><path d="M9 5a3 3 0 0 1 6 0v4"/><path d="M9 10v1a3 3 0 0 0 5.1 2.1"/><path d="M19 10a7 7 0 0 1-1.24 3.97"/><path d="M5 10a7 7 0 0 0 11 5.67"/><line x1="12" y1="19" x2="12" y2="22"/></svg>
|
<svg class="mic-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="2" x2="22" y2="22"/><path d="M9 5a3 3 0 0 1 6 0v4"/><path d="M9 10v1a3 3 0 0 0 5.1 2.1"/><path d="M19 10a7 7 0 0 1-1.24 3.97"/><path d="M5 10a7 7 0 0 0 11 5.67"/><line x1="12" y1="19" x2="12" y2="22"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
id="main-circle"
|
id="main-circle"
|
||||||
class="circle state-idle"
|
class="circle state-idle"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Start voice conversation"
|
aria-label="Iniciar conversación de voz"
|
||||||
>
|
>
|
||||||
<span class="circle-glow" aria-hidden="true"></span>
|
<span class="circle-glow" aria-hidden="true"></span>
|
||||||
<span class="circle-ring" aria-hidden="true"></span>
|
<span class="circle-ring" aria-hidden="true"></span>
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="stop-btn" class="side-btn" type="button" aria-label="End" title="End" aria-hidden="true">
|
<button id="stop-btn" class="side-btn" type="button" aria-label="Finalizar" title="Finalizar" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,15 +324,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id="circle-caption" class="circle-caption" role="status">Tap to start</p>
|
<p id="circle-caption" class="circle-caption" role="status">Toca para empezar</p>
|
||||||
<p id="circle-subcaption" class="circle-subcaption" hidden></p>
|
<p id="circle-subcaption" class="circle-subcaption" hidden></p>
|
||||||
|
|
||||||
<div id="queue-actions" class="queue-actions" hidden>
|
<div id="queue-actions" class="queue-actions" hidden>
|
||||||
<button id="join-queue-btn" class="join-queue-btn" type="button" hidden>
|
<button id="join-queue-btn" class="join-queue-btn" type="button" hidden>
|
||||||
Join now
|
Entrar ahora
|
||||||
</button>
|
</button>
|
||||||
<button id="leave-queue-btn" class="leave-queue-btn" type="button" hidden>
|
<button id="leave-queue-btn" class="leave-queue-btn" type="button" hidden>
|
||||||
Leave queue
|
Salir de la cola
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
<div id="cam-pip" class="cam-pip" aria-hidden="true" hidden>
|
<div id="cam-pip" class="cam-pip" aria-hidden="true" hidden>
|
||||||
<video id="cam-video" class="cam-video" autoplay playsinline muted></video>
|
<video id="cam-video" class="cam-video" autoplay playsinline muted></video>
|
||||||
<span class="cam-flash" aria-hidden="true"></span>
|
<span class="cam-flash" aria-hidden="true"></span>
|
||||||
<span class="cam-label">camera</span>
|
<span class="cam-label">cámara</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Left sidebar: scheduled reports.
|
<!-- Left sidebar: scheduled reports.
|
||||||
@@ -458,8 +458,8 @@
|
|||||||
<dialog id="about-modal" class="modal about-modal">
|
<dialog id="about-modal" class="modal about-modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>About</h2>
|
<h2>Acerca de</h2>
|
||||||
<button id="about-close" class="icon-btn" aria-label="Close">
|
<button id="about-close" class="icon-btn" aria-label="Cerrar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -470,29 +470,29 @@
|
|||||||
|
|
||||||
<!-- The backend, shown as the path a turn actually travels -->
|
<!-- The backend, shown as the path a turn actually travels -->
|
||||||
<div class="about-pipeline">
|
<div class="about-pipeline">
|
||||||
<p class="pipeline-title">The pipeline</p>
|
<p class="pipeline-title">Cómo funciona</p>
|
||||||
<ol class="pipeline">
|
<ol class="pipeline">
|
||||||
<li class="pipe-endpoint">You speak</li>
|
<li class="pipe-endpoint">Tú hablas</li>
|
||||||
<li class="pipe-stage">
|
<li class="pipe-stage">
|
||||||
<span class="pipe-tag">VAD</span>
|
<span class="pipe-tag">VAD</span>
|
||||||
<span class="pipe-job">detects speech</span>
|
<span class="pipe-job">detecta el habla</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="pipe-stage">
|
<li class="pipe-stage">
|
||||||
<span class="pipe-tag">STT</span>
|
<span class="pipe-tag">STT</span>
|
||||||
<span class="pipe-job">transcribes it</span>
|
<span class="pipe-job">la transcribe</span>
|
||||||
<a class="pipe-model" href="https://huggingface.co/nvidia/parakeet-tdt-1.1b" target="_blank" rel="noopener">nvidia/parakeet-tdt-1.1b<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
<a class="pipe-model" href="https://huggingface.co/nvidia/parakeet-tdt-1.1b" target="_blank" rel="noopener">nvidia/parakeet-tdt-1.1b<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="pipe-stage">
|
<li class="pipe-stage">
|
||||||
<span class="pipe-tag">VLM</span>
|
<span class="pipe-tag">VLM</span>
|
||||||
<span class="pipe-job">analyses and composes the reply <span class="pipe-note">· via <a class="cerebras-credit" href="https://cerebras.ai" target="_blank" rel="noopener"><svg class="cerebras-mark" viewBox="0 0 50 50" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M29.4186 5.62713C24.2806 5.62713 19.353 7.6682 15.7199 11.3013C12.0868 14.9345 10.0457 19.862 10.0457 25C10.0457 30.1381 12.0868 35.0656 15.7199 38.6988C19.353 42.3319 24.2806 44.373 29.4186 44.373V47.2917C17.1061 47.2917 7.12695 37.3105 7.12695 24.998C7.12695 12.6855 17.104 2.7063 29.4165 2.7063V5.62505L29.4186 5.62713ZM39.3186 13.2875C37.7794 11.9809 35.9971 10.9915 34.0741 10.3761C32.1512 9.76067 30.1256 9.53147 28.1137 9.70166C26.1019 9.87185 24.1435 10.4381 22.3513 11.3677C20.559 12.2974 18.9683 13.5722 17.6704 15.1189C16.3726 16.6655 15.3933 18.4534 14.7888 20.3798C14.1844 22.3062 13.9667 24.3332 14.1483 26.344C14.33 28.3548 14.9073 30.3099 15.8472 32.0968C16.787 33.8838 18.0709 35.4673 19.6249 36.7563L17.7478 38.9938C15.9126 37.4545 14.3987 35.5687 13.2924 33.4442C12.1862 31.3197 11.5092 28.9981 11.3003 26.6119C11.0914 24.2258 11.3545 21.8219 12.0747 19.5374C12.7949 17.253 13.9581 15.1328 15.4978 13.298C17.0373 11.4628 18.9233 9.94877 21.0479 8.84247C23.1726 7.73618 25.4944 7.05923 27.8807 6.8503C30.267 6.64136 32.6711 6.90453 34.9557 7.62477C37.2403 8.34501 39.3607 9.50822 41.1957 11.048L39.3186 13.2875ZM34.6207 15.0459C31.9818 13.6829 28.9113 13.4172 26.0776 14.3068C23.2438 15.1964 20.876 17.1691 19.4895 19.7958C18.1029 22.4224 17.8099 25.4903 18.6741 28.332C19.5384 31.1736 21.4899 33.5589 24.104 34.9688L22.7374 37.5521C19.4721 35.7617 17.0418 32.7592 15.9707 29.1927C14.8996 25.6261 15.2737 21.7815 17.0123 18.4883C18.7509 15.1952 21.7146 12.7176 25.2637 11.5903C28.8129 10.463 32.663 10.7763 35.9832 12.4625L34.6207 15.0459ZM29.4165 17.7896C27.5042 17.7896 25.6702 18.5493 24.318 19.9015C22.9658 21.2537 22.2061 23.0877 22.2061 25C22.2061 26.9124 22.9658 28.7464 24.318 30.0986C25.6702 31.4508 27.5042 32.2105 29.4165 32.2105V35.1313C26.7296 35.1313 24.1526 34.0639 22.2527 32.1639C20.3527 30.2639 19.2853 27.687 19.2853 25C19.2853 22.3131 20.3527 19.7362 22.2527 17.8362C24.1526 15.9362 26.7296 14.8688 29.4165 14.8688V17.7896Z" fill="#F15A29"/><path d="M32.0981 22.5749C31.7875 22.2404 31.4295 21.9534 31.0356 21.7228C30.6925 21.519 30.3014 21.4097 29.9023 21.4062C29.371 21.4062 28.896 21.5041 28.4773 21.6978C28.0725 21.8802 27.7089 22.1425 27.4081 22.469C27.1074 22.7955 26.8758 23.1795 26.7273 23.5978C26.5731 24.0207 26.4981 24.4645 26.4981 24.9124C26.4981 25.3666 26.5731 25.8082 26.7273 26.227C26.877 26.6449 27.1091 27.0286 27.4096 27.3553C27.7102 27.682 28.0733 27.9451 28.4773 28.1291C28.8939 28.3228 29.3731 28.4207 29.9023 28.4207C30.3523 28.4207 30.771 28.3249 31.1564 28.1395C31.5481 27.952 31.8856 27.6707 32.146 27.3228L34.0794 29.4187C33.7877 29.7103 33.4544 29.9624 33.0752 30.1749C32.374 30.566 31.6108 30.8338 30.8189 30.9666C30.4648 31.0207 30.1585 31.0499 29.9023 31.0499C29.0617 31.0552 28.2271 30.9069 27.4398 30.6124C26.6954 30.3371 26.0144 29.914 25.4377 29.3687C24.8643 28.8208 24.4078 28.1624 24.096 27.4332C23.7572 26.6366 23.5883 25.778 23.6002 24.9124C23.6002 23.9874 23.7669 23.1478 24.096 22.3916C24.4085 21.6624 24.8627 21.0041 25.4356 20.4562C26.0148 19.9124 26.696 19.4895 27.4398 19.2145C28.2271 18.92 29.0617 18.7717 29.9023 18.777C30.6419 18.777 31.3856 18.9187 32.1356 19.202C32.8877 19.4895 33.5627 19.952 34.1023 20.5541L32.0981 22.5749Z" fill="#fff"/></svg>Cerebras</a></span></span>
|
<span class="pipe-job">analiza y redacta la respuesta <span class="pipe-note">· mediante <a class="cerebras-credit" href="https://cerebras.ai" target="_blank" rel="noopener"><svg class="cerebras-mark" viewBox="0 0 50 50" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M29.4186 5.62713C24.2806 5.62713 19.353 7.6682 15.7199 11.3013C12.0868 14.9345 10.0457 19.862 10.0457 25C10.0457 30.1381 12.0868 35.0656 15.7199 38.6988C19.353 42.3319 24.2806 44.373 29.4186 44.373V47.2917C17.1061 47.2917 7.12695 37.3105 7.12695 24.998C7.12695 12.6855 17.104 2.7063 29.4165 2.7063V5.62505L29.4186 5.62713ZM39.3186 13.2875C37.7794 11.9809 35.9971 10.9915 34.0741 10.3761C32.1512 9.76067 30.1256 9.53147 28.1137 9.70166C26.1019 9.87185 24.1435 10.4381 22.3513 11.3677C20.559 12.2974 18.9683 13.5722 17.6704 15.1189C16.3726 16.6655 15.3933 18.4534 14.7888 20.3798C14.1844 22.3062 13.9667 24.3332 14.1483 26.344C14.33 28.3548 14.9073 30.3099 15.8472 32.0968C16.787 33.8838 18.0709 35.4673 19.6249 36.7563L17.7478 38.9938C15.9126 37.4545 14.3987 35.5687 13.2924 33.4442C12.1862 31.3197 11.5092 28.9981 11.3003 26.6119C11.0914 24.2258 11.3545 21.8219 12.0747 19.5374C12.7949 17.253 13.9581 15.1328 15.4978 13.298C17.0373 11.4628 18.9233 9.94877 21.0479 8.84247C23.1726 7.73618 25.4944 7.05923 27.8807 6.8503C30.267 6.64136 32.6711 6.90453 34.9557 7.62477C37.2403 8.34501 39.3607 9.50822 41.1957 11.048L39.3186 13.2875ZM34.6207 15.0459C31.9818 13.6829 28.9113 13.4172 26.0776 14.3068C23.2438 15.1964 20.876 17.1691 19.4895 19.7958C18.1029 22.4224 17.8099 25.4903 18.6741 28.332C19.5384 31.1736 21.4899 33.5589 24.104 34.9688L22.7374 37.5521C19.4721 35.7617 17.0418 32.7592 15.9707 29.1927C14.8996 25.6261 15.2737 21.7815 17.0123 18.4883C18.7509 15.1952 21.7146 12.7176 25.2637 11.5903C28.8129 10.463 32.663 10.7763 35.9832 12.4625L34.6207 15.0459ZM29.4165 17.7896C27.5042 17.7896 25.6702 18.5493 24.318 19.9015C22.9658 21.2537 22.2061 23.0877 22.2061 25C22.2061 26.9124 22.9658 28.7464 24.318 30.0986C25.6702 31.4508 27.5042 32.2105 29.4165 32.2105V35.1313C26.7296 35.1313 24.1526 34.0639 22.2527 32.1639C20.3527 30.2639 19.2853 27.687 19.2853 25C19.2853 22.3131 20.3527 19.7362 22.2527 17.8362C24.1526 15.9362 26.7296 14.8688 29.4165 14.8688V17.7896Z" fill="#F15A29"/><path d="M32.0981 22.5749C31.7875 22.2404 31.4295 21.9534 31.0356 21.7228C30.6925 21.519 30.3014 21.4097 29.9023 21.4062C29.371 21.4062 28.896 21.5041 28.4773 21.6978C28.0725 21.8802 27.7089 22.1425 27.4081 22.469C27.1074 22.7955 26.8758 23.1795 26.7273 23.5978C26.5731 24.0207 26.4981 24.4645 26.4981 24.9124C26.4981 25.3666 26.5731 25.8082 26.7273 26.227C26.877 26.6449 27.1091 27.0286 27.4096 27.3553C27.7102 27.682 28.0733 27.9451 28.4773 28.1291C28.8939 28.3228 29.3731 28.4207 29.9023 28.4207C30.3523 28.4207 30.771 28.3249 31.1564 28.1395C31.5481 27.952 31.8856 27.6707 32.146 27.3228L34.0794 29.4187C33.7877 29.7103 33.4544 29.9624 33.0752 30.1749C32.374 30.566 31.6108 30.8338 30.8189 30.9666C30.4648 31.0207 30.1585 31.0499 29.9023 31.0499C29.0617 31.0552 28.2271 30.9069 27.4398 30.6124C26.6954 30.3371 26.0144 29.914 25.4377 29.3687C24.8643 28.8208 24.4078 28.1624 24.096 27.4332C23.7572 26.6366 23.5883 25.778 23.6002 24.9124C23.6002 23.9874 23.7669 23.1478 24.096 22.3916C24.4085 21.6624 24.8627 21.0041 25.4356 20.4562C26.0148 19.9124 26.696 19.4895 27.4398 19.2145C28.2271 18.92 29.0617 18.7717 29.9023 18.777C30.6419 18.777 31.3856 18.9187 32.1356 19.202C32.8877 19.4895 33.5627 19.952 34.1023 20.5541L32.0981 22.5749Z" fill="#fff"/></svg>Cerebras</a></span></span>
|
||||||
<a class="pipe-model" href="https://huggingface.co/google/gemma-4-31B-it" target="_blank" rel="noopener">google/gemma-4-31B-it<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
<a class="pipe-model" href="https://huggingface.co/google/gemma-4-31B-it" target="_blank" rel="noopener">google/gemma-4-31B-it<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="pipe-stage">
|
<li class="pipe-stage">
|
||||||
<span class="pipe-tag">TTS</span>
|
<span class="pipe-tag">TTS</span>
|
||||||
<span class="pipe-job">speaks it back</span>
|
<span class="pipe-job">la convierte en voz</span>
|
||||||
<a class="pipe-model" href="https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice" target="_blank" rel="noopener">Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
<a class="pipe-model" href="https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice" target="_blank" rel="noopener">Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice<svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M8 7h9v9"/></svg></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="pipe-endpoint">The orb replies</li>
|
<li class="pipe-endpoint">Vocero responde</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -501,8 +501,8 @@
|
|||||||
<dialog id="settings-modal" class="modal">
|
<dialog id="settings-modal" class="modal">
|
||||||
<form method="dialog" class="modal-content">
|
<form method="dialog" class="modal-content">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>Settings</h2>
|
<h2>Configuración</h2>
|
||||||
<button class="icon-btn" value="close" aria-label="Close">
|
<button class="icon-btn" value="close" aria-label="Cerrar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -510,17 +510,17 @@
|
|||||||
<div class="tab-panels">
|
<div class="tab-panels">
|
||||||
<div class="tab-panel active" role="tabpanel">
|
<div class="tab-panel active" role="tabpanel">
|
||||||
<label class="field" id="conn-field">
|
<label class="field" id="conn-field">
|
||||||
<span id="conn-label">Speech-to-speech server URL</span>
|
<span id="conn-label">URL del servidor de voz</span>
|
||||||
<input id="lb-url" type="text" autocomplete="off" spellcheck="false" placeholder="http://localhost:port" />
|
<input id="lb-url" type="text" autocomplete="off" spellcheck="false" placeholder="http://localhost:8080" />
|
||||||
<small id="conn-hint">
|
<small id="conn-hint">
|
||||||
URL of your speech-to-speech server, e.g.
|
URL de tu servidor de voz, por ejemplo
|
||||||
<code>http://localhost:8080</code> (the app adds <code>/v1/realtime</code>).
|
<code>http://localhost:8080</code> (la aplicación añade <code>/v1/realtime</code>).
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="field-row">
|
<div class="field-row">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Voice</span>
|
<span>Voz</span>
|
||||||
<select id="voice">
|
<select id="voice">
|
||||||
<option value="Aiden" selected>Aiden</option>
|
<option value="Aiden" selected>Aiden</option>
|
||||||
<option value="Ryan">Ryan</option>
|
<option value="Ryan">Ryan</option>
|
||||||
@@ -537,8 +537,8 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="field-head">
|
<span class="field-head">
|
||||||
Noise gate
|
Puerta de ruido
|
||||||
<span id="gate-value" class="field-value">Off</span>
|
<span id="gate-value" class="field-value">Desactivada</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="gate">
|
<div class="gate">
|
||||||
<div class="gate-track" aria-hidden="true">
|
<div class="gate-track" aria-hidden="true">
|
||||||
@@ -550,37 +550,37 @@
|
|||||||
max="-3"
|
max="-3"
|
||||||
step="1"
|
step="1"
|
||||||
value="-50"
|
value="-50"
|
||||||
aria-label="Noise gate threshold"
|
aria-label="Umbral de la puerta de ruido"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="gate-ends">
|
<div class="gate-ends">
|
||||||
<span>Off</span>
|
<span>Desactivada</span>
|
||||||
<span>−3 dB</span>
|
<span>−3 dB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
Mutes the mic below the handle so room noise isn't sent. Slide fully
|
Silencia el micrófono por debajo del umbral para evitar enviar ruido.
|
||||||
left to turn it off; the bar shows your live input while in a call.
|
Desliza hasta la izquierda para desactivarla. La barra muestra tu audio durante la llamada.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Instructions</span>
|
<span>Instrucciones</span>
|
||||||
<textarea id="instructions" rows="5" placeholder="You are Vocero, a friendly voice assistant for the company Aquachile..."></textarea>
|
<textarea id="instructions" rows="5" placeholder="Introduce las instrucciones de Vocero para esta conversación…"></textarea>
|
||||||
<small id="primary-prompt-meta" hidden></small>
|
<small id="primary-prompt-meta" hidden></small>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<button id="restart-conversation" type="button" class="btn primary wide" disabled>
|
<button id="restart-conversation" type="button" class="btn primary wide" disabled>
|
||||||
Restart conversation with these settings
|
Reiniciar conversación con esta configuración
|
||||||
</button>
|
</button>
|
||||||
<small id="restart-hint">Connect first, then come back here to apply live changes.</small>
|
<small id="restart-hint">Conéctate primero y vuelve aquí para aplicar cambios durante la conversación.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="modal-footer">
|
<footer class="modal-footer">
|
||||||
<button id="settings-save" type="submit" class="btn primary" value="save">Save</button>
|
<button id="settings-save" type="submit" class="btn primary" value="save">Guardar</button>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -588,19 +588,19 @@
|
|||||||
<dialog id="tools-modal" class="modal">
|
<dialog id="tools-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>Tools</h2>
|
<h2>Herramientas</h2>
|
||||||
<button id="tools-close" class="icon-btn" aria-label="Close">
|
<button id="tools-close" class="icon-btn" aria-label="Cerrar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="tools-intro">Let the assistant act during the conversation. Changes apply live.</p>
|
<p class="tools-intro">Permite que el asistente actúe durante la conversación. Los cambios se aplican de inmediato.</p>
|
||||||
|
|
||||||
<div class="tool-list">
|
<div class="tool-list">
|
||||||
<div class="tool-row" id="tool-web-row">
|
<div class="tool-row" id="tool-web-row">
|
||||||
<div class="tool-info">
|
<div class="tool-info">
|
||||||
<span class="tool-name">Web search</span>
|
<span class="tool-name">Búsqueda web</span>
|
||||||
<span class="tool-desc">Look things up on Google, via Serper.</span>
|
<span class="tool-desc">Busca información en Google mediante Serper.</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input id="tool-web" type="checkbox" />
|
<input id="tool-web" type="checkbox" />
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field tools-key">
|
<label class="field tools-key">
|
||||||
<span>Search API key</span>
|
<span>Clave de la API de búsqueda</span>
|
||||||
<input id="search-key" type="password" autocomplete="off" spellcheck="false" />
|
<input id="search-key" type="password" autocomplete="off" spellcheck="false" />
|
||||||
<small id="tool-web-hint"></small>
|
<small id="tool-web-hint"></small>
|
||||||
</label>
|
</label>
|
||||||
@@ -618,20 +618,20 @@
|
|||||||
then restore the toolCamSwitch listener and enableCamera functions in main.js. -->
|
then restore the toolCamSwitch listener and enableCamera functions in main.js. -->
|
||||||
<div class="tool-row tool-row-sep" id="tool-cam-row" hidden>
|
<div class="tool-row tool-row-sep" id="tool-cam-row" hidden>
|
||||||
<div class="tool-info">
|
<div class="tool-info">
|
||||||
<span class="tool-name">Camera</span>
|
<span class="tool-name">Cámara</span>
|
||||||
<span class="tool-desc">Let the assistant see through your webcam.</span>
|
<span class="tool-desc">Permite que el asistente vea a través de tu cámara.</span>
|
||||||
</div>
|
</div>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input id="tool-cam" type="checkbox" />
|
<input id="tool-cam" type="checkbox" />
|
||||||
<span class="switch-track" aria-hidden="true"></span>
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<small id="tool-cam-hint" class="tool-hint" hidden>A live preview shows bottom-left while the camera is on.</small>
|
<small id="tool-cam-hint" class="tool-hint" hidden>La vista previa aparece abajo a la izquierda mientras la cámara está activa.</small>
|
||||||
|
|
||||||
<div class="tool-row tool-row-sep">
|
<div class="tool-row tool-row-sep">
|
||||||
<div class="tool-info">
|
<div class="tool-info">
|
||||||
<span class="tool-name">Loaded MCP tools</span>
|
<span class="tool-name">Herramientas MCP cargadas</span>
|
||||||
<span class="tool-desc">Tools published by the remote agent profile.</span>
|
<span class="tool-desc">Herramientas publicadas por el perfil remoto del agente.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small id="agent-tools-hint" class="tool-hint"></small>
|
<small id="agent-tools-hint" class="tool-hint"></small>
|
||||||
@@ -643,36 +643,36 @@
|
|||||||
<dialog id="rag-modal" class="modal">
|
<dialog id="rag-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<header class="modal-header">
|
<header class="modal-header">
|
||||||
<h2>Custom RAG context</h2>
|
<h2>Contexto RAG personalizado</h2>
|
||||||
<button id="rag-close" class="icon-btn" aria-label="Close">
|
<button id="rag-close" class="icon-btn" aria-label="Cerrar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<p class="tools-intro">
|
<p class="tools-intro">
|
||||||
Incolla qui testo, documenti, o estratti RAG che vuoi siano disponibili
|
Pega aquí texto, documentos o fragmentos RAG que quieras tener disponibles
|
||||||
per la sessione vocale corrente. Il testo viene aggiunto sia alle
|
durante la sesión de voz actual. El texto se añade a las
|
||||||
istruzioni del LLM sia al campo <code>user_context</code> del body
|
instrucciones del modelo y al campo <code>user_context</code> de la solicitud a
|
||||||
<code>/api/reasoning</code>.
|
<code>/api/reasoning</code>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>RAG context (testo libero, puro)</span>
|
<span>Contexto RAG (texto libre)</span>
|
||||||
<textarea
|
<textarea
|
||||||
id="rag-textarea"
|
id="rag-textarea"
|
||||||
rows="12"
|
rows="12"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
placeholder="Incolla o scrivi qui il contesto RAG che il modello deve usare durante la risposta. Es: dati cliente, policy, estratti documentali, ecc."
|
placeholder="Pega o escribe el contexto RAG que el modelo debe usar al responder. Por ejemplo: datos del cliente, políticas o fragmentos de documentos."
|
||||||
></textarea>
|
></textarea>
|
||||||
<small id="rag-hint" class="tool-hint">
|
<small id="rag-hint" class="tool-hint">
|
||||||
Nessun RAG context caricato. Premi "Invia nella sessione" per applicare le modifiche.
|
No hay contexto RAG cargado. Pulsa "Enviar a la sesión" para aplicar los cambios.
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<footer class="modal-footer">
|
<footer class="modal-footer">
|
||||||
<button id="rag-clear" class="btn secondary" type="button" value="clear">Svuota</button>
|
<button id="rag-clear" class="btn secondary" type="button" value="clear">Vaciar</button>
|
||||||
<div style="flex: 1"></div>
|
<div style="flex: 1"></div>
|
||||||
<button id="rag-submit" class="btn primary" type="button" value="submit">Invia nella sessione</button>
|
<button id="rag-submit" class="btn primary" type="button" value="submit">Enviar a la sesión</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@@ -682,13 +682,13 @@
|
|||||||
ui/account.js. The hero is the HF mark — a friendly smiling face. -->
|
ui/account.js. The hero is the HF mark — a friendly smiling face. -->
|
||||||
<dialog id="limit-modal" class="modal limit-modal">
|
<dialog id="limit-modal" class="modal limit-modal">
|
||||||
<div class="modal-content limit-card">
|
<div class="modal-content limit-card">
|
||||||
<button id="limit-close" class="icon-btn limit-close" aria-label="Close">
|
<button id="limit-close" class="icon-btn limit-close" aria-label="Cerrar">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="limit-badge" aria-hidden="true">
|
<div class="limit-badge" aria-hidden="true">
|
||||||
<svg class="hf-logo" viewBox="0 0 95 88" fill="none" aria-hidden="true"><path fill="#FFD21E" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5Z"/><path fill="#FF9D0B" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0Zm-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0Z"/><path fill="#3A3B45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32ZM34.95 32.3c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/><path fill="#FF323D" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/><path fill="#3A3B45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.61 8.61 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97Z" clip-rule="evenodd"/><path fill="#FF9D0B" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM24.21 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM17.52 48c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7.1 7.1 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.33 44.33 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48Zm0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85Z"/><path fill="#FFD21E" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9c-1.87.3-3.24 5.08.68 8.01 3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12Z"/><path fill="#FF9D0B" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7.1 7.1 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.33 44.33 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48Zm0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.54 1.54 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85Z"/><path fill="#FFD21E" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44c2.08 2.27-1.58 4.17-6.8 7.33-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12Z"/></svg>
|
<svg class="hf-logo" viewBox="0 0 95 88" fill="none" aria-hidden="true"><path fill="#FFD21E" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5Z"/><path fill="#FF9D0B" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0Zm-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0Z"/><path fill="#3A3B45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32ZM34.95 32.3c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/><path fill="#FF323D" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/><path fill="#3A3B45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.61 8.61 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97Z" clip-rule="evenodd"/><path fill="#FF9D0B" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM24.21 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5ZM17.52 48c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7.1 7.1 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.33 44.33 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48Zm0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85Z"/><path fill="#FFD21E" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9c-1.87.3-3.24 5.08.68 8.01 3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12Z"/><path fill="#FF9D0B" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7.1 7.1 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.33 44.33 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48Zm0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.54 1.54 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85Z"/><path fill="#FFD21E" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44c2.08 2.27-1.58 4.17-6.8 7.33-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12Z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="limit-title" class="limit-title">That's a wrap for now</h2>
|
<h2 id="limit-title" class="limit-title">Por ahora hemos terminado</h2>
|
||||||
<p id="limit-msg" class="limit-msg"></p>
|
<p id="limit-msg" class="limit-msg"></p>
|
||||||
<a id="limit-cta" class="btn primary wide limit-cta" href="#"></a>
|
<a id="limit-cta" class="btn primary wide limit-cta" href="#"></a>
|
||||||
<p id="limit-note" class="limit-note"></p>
|
<p id="limit-note" class="limit-note"></p>
|
||||||
@@ -700,7 +700,7 @@
|
|||||||
user metadata populated via main.js populateUserProfileDialog(). -->
|
user metadata populated via main.js populateUserProfileDialog(). -->
|
||||||
<dialog id="user-profile-dialog" class="modal user-profile-modal" aria-labelledby="user-profile-title">
|
<dialog id="user-profile-dialog" class="modal user-profile-modal" aria-labelledby="user-profile-title">
|
||||||
<div class="modal-content user-profile-card">
|
<div class="modal-content user-profile-card">
|
||||||
<button id="user-profile-close" class="icon-btn user-profile-close" aria-label="Close user profile">
|
<button id="user-profile-close" class="icon-btn user-profile-close" aria-label="Cerrar perfil de usuario">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -714,12 +714,12 @@
|
|||||||
<!-- Metadata: a 1-col definition-list-like grid. Empty rows are hidden
|
<!-- Metadata: a 1-col definition-list-like grid. Empty rows are hidden
|
||||||
automatically via JS when the field has no value, so we never show
|
automatically via JS when the field has no value, so we never show
|
||||||
blank labels. -->
|
blank labels. -->
|
||||||
<div id="user-profile-metadata" class="user-profile-metadata" aria-label="Informazioni utente"></div>
|
<div id="user-profile-metadata" class="user-profile-metadata" aria-label="Información del usuario"></div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<footer class="app-footer" aria-label="App version footer">
|
<footer class="app-footer" aria-label="Versión de la aplicación">
|
||||||
<span class="app-footer-text">Azurian Vocero realtime ver.Beta 2.1</span>
|
<span class="app-footer-text">Azurian Vocero en tiempo real ver. Beta 2.1</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script type="module" src="main.js?v=202609102207"></script>
|
<script type="module" src="main.js?v=202609102207"></script>
|
||||||
|
|||||||
+47
-47
@@ -627,15 +627,15 @@ function saveTools() {
|
|||||||
|
|
||||||
/** @type {Record<AppState, { caption: string; disabled: boolean }>} */
|
/** @type {Record<AppState, { caption: string; disabled: boolean }>} */
|
||||||
const STATE_VIEWS = {
|
const STATE_VIEWS = {
|
||||||
idle: { caption: "Tap or say \"Hola Vocero\"", disabled: false },
|
idle: { caption: "Toca o di \"Hola Vocero\"", disabled: false },
|
||||||
connecting: { caption: "Connecting", disabled: true },
|
connecting: { caption: "Conectando", disabled: true },
|
||||||
queued: { caption: "Finding you a spot…", disabled: true },
|
queued: { caption: "Buscando un lugar para ti…", disabled: true },
|
||||||
"your-turn": { caption: "You're up! 🎉", disabled: true },
|
"your-turn": { caption: "¡Es tu turno! 🎉", disabled: true },
|
||||||
listening: { caption: "", disabled: false },
|
listening: { caption: "", disabled: false },
|
||||||
"user-speaking": { caption: "", disabled: false },
|
"user-speaking": { caption: "", disabled: false },
|
||||||
processing: { caption: "", disabled: false },
|
processing: { caption: "", disabled: false },
|
||||||
"ai-speaking": { caption: "", disabled: false },
|
"ai-speaking": { caption: "", disabled: false },
|
||||||
error: { caption: "Tap to retry", disabled: false },
|
error: { caption: "Toca para reintentar", disabled: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {Record<AppState, string>} */
|
/** @type {Record<AppState, string>} */
|
||||||
@@ -885,11 +885,11 @@ function pushCustomRagToLiveSessionIfAny() {
|
|||||||
function syncRagHintFooter() {
|
function syncRagHintFooter() {
|
||||||
if (!ragHint) return;
|
if (!ragHint) return;
|
||||||
if (!customRagLoaded || !customRagText.trim()) {
|
if (!customRagLoaded || !customRagText.trim()) {
|
||||||
ragHint.textContent = "Nessun RAG context caricato. Premi \"Invia nella sessione\" per applicare le modifiche.";
|
ragHint.textContent = "No hay contexto RAG cargado. Pulsa \"Enviar a la sesión\" para aplicar los cambios.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const lines = customRagText.split(/\r?\n/).length;
|
const lines = customRagText.split(/\r?\n/).length;
|
||||||
ragHint.textContent = `✅ RAG context caricato: ${customRagText.length} chars / ${lines} linee. Viene aggiunto a istruzioni LLM e user_context.`;
|
ragHint.textContent = `✅ Contexto RAG cargado: ${customRagText.length} caracteres / ${lines} líneas. Se añade a las instrucciones del modelo y a user_context.`;
|
||||||
}
|
}
|
||||||
// Dedupe cache for [INJECT-PROFILE] browser logs (avoid 20 logs per click).
|
// Dedupe cache for [INJECT-PROFILE] browser logs (avoid 20 logs per click).
|
||||||
let _lastInjectProfileLogKey = "";
|
let _lastInjectProfileLogKey = "";
|
||||||
@@ -1073,7 +1073,7 @@ function buildProfileRows(profile) {
|
|||||||
"email", "emailAddress", "email_address", "eMail", "mail", "contactEmail",
|
"email", "emailAddress", "email_address", "eMail", "mail", "contactEmail",
|
||||||
"user_email", "customer_email", "loginEmail", "username_email",
|
"user_email", "customer_email", "loginEmail", "username_email",
|
||||||
]);
|
]);
|
||||||
if (email) rows.push({ label: "Email", value: email, isEmail: true, href: `mailto:${email}` });
|
if (email) rows.push({ label: "Correo electrónico", value: email, isEmail: true, href: `mailto:${email}` });
|
||||||
|
|
||||||
// Phone (linkable tel)
|
// Phone (linkable tel)
|
||||||
const phone = firstProfileField(profile, [
|
const phone = firstProfileField(profile, [
|
||||||
@@ -1086,33 +1086,33 @@ function buildProfileRows(profile) {
|
|||||||
const username = firstProfileField(profile, [
|
const username = firstProfileField(profile, [
|
||||||
"username", "userName", "user_name", "login", "handle", "nickname", "screenName", "accountName",
|
"username", "userName", "user_name", "login", "handle", "nickname", "screenName", "accountName",
|
||||||
]);
|
]);
|
||||||
if (username) rows.push({ label: "Username", value: username, mono: true });
|
if (username) rows.push({ label: "Nombre de usuario", value: username, mono: true });
|
||||||
|
|
||||||
// Role / Plan
|
// Role / Plan
|
||||||
const role = firstProfileField(profile, [
|
const role = firstProfileField(profile, [
|
||||||
"role", "roleName", "userRole", "plan", "planName", "tier", "subscription", "membership", "accessLevel",
|
"role", "roleName", "userRole", "plan", "planName", "tier", "subscription", "membership", "accessLevel",
|
||||||
]);
|
]);
|
||||||
if (role) rows.push({ label: "Ruolo / Piano", value: role });
|
if (role) rows.push({ label: "Rol / Plan", value: role });
|
||||||
|
|
||||||
// Company / Organization
|
// Company / Organization
|
||||||
const company = firstProfileField(profile, [
|
const company = firstProfileField(profile, [
|
||||||
"company", "companyName", "company_name", "organization", "org", "organizationName", "business",
|
"company", "companyName", "company_name", "organization", "org", "organizationName", "business",
|
||||||
]);
|
]);
|
||||||
if (company) rows.push({ label: "Azienda", value: company });
|
if (company) rows.push({ label: "Empresa", value: company });
|
||||||
|
|
||||||
// User/Account ID (mono for monospace copy-paste)
|
// User/Account ID (mono for monospace copy-paste)
|
||||||
const userId = firstProfileField(profile, [
|
const userId = firstProfileField(profile, [
|
||||||
"userId", "user_id", "uid", "accountId", "account_id", "sub", "customerId", "customer_id",
|
"userId", "user_id", "uid", "accountId", "account_id", "sub", "customerId", "customer_id",
|
||||||
"id", "userPk", "profileId", "profile_id", "identityId",
|
"id", "userPk", "profileId", "profile_id", "identityId",
|
||||||
]);
|
]);
|
||||||
if (userId) rows.push({ label: "ID Utente", value: userId, mono: true });
|
if (userId) rows.push({ label: "ID de usuario", value: userId, mono: true });
|
||||||
|
|
||||||
// Locale / Country
|
// Locale / Country
|
||||||
const country = firstProfileField(profile, [
|
const country = firstProfileField(profile, [
|
||||||
"country", "countryName", "country_code", "countryCode", "locale", "language", "lang",
|
"country", "countryName", "country_code", "countryCode", "locale", "language", "lang",
|
||||||
"region", "timezone", "tz",
|
"region", "timezone", "tz",
|
||||||
]);
|
]);
|
||||||
if (country) rows.push({ label: "Nazione / Lingua", value: country });
|
if (country) rows.push({ label: "País / Idioma", value: country });
|
||||||
|
|
||||||
// Created at / signed up date — try common date fields.
|
// Created at / signed up date — try common date fields.
|
||||||
const createdAtKeys = [
|
const createdAtKeys = [
|
||||||
@@ -1130,7 +1130,7 @@ function buildProfileRows(profile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (createdAt) rows.push({ label: "Iscritto il", value: createdAt, isDate: true });
|
if (createdAt) rows.push({ label: "Fecha de registro", value: createdAt, isDate: true });
|
||||||
|
|
||||||
// Last sign in / active date (last_login etc)
|
// Last sign in / active date (last_login etc)
|
||||||
const lastActiveKeys = [
|
const lastActiveKeys = [
|
||||||
@@ -1146,7 +1146,7 @@ function buildProfileRows(profile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lastActive) rows.push({ label: "Ultimo accesso", value: lastActive, isDate: true });
|
if (lastActive) rows.push({ label: "Último acceso", value: lastActive, isDate: true });
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -1160,16 +1160,16 @@ function populateUserProfileDialog(profile) {
|
|||||||
const initials = initialsForName(name);
|
const initials = initialsForName(name);
|
||||||
if (userProfileAvatar) {
|
if (userProfileAvatar) {
|
||||||
userProfileAvatar.textContent = initials || "??";
|
userProfileAvatar.textContent = initials || "??";
|
||||||
userProfileAvatar.setAttribute("aria-label", name ? `Avatar: ${name}` : "Utente non impostato");
|
userProfileAvatar.setAttribute("aria-label", name ? `Avatar: ${name}` : "Usuario no identificado");
|
||||||
}
|
}
|
||||||
if (userProfileTitle) {
|
if (userProfileTitle) {
|
||||||
userProfileTitle.textContent = name || "Utente non impostato";
|
userProfileTitle.textContent = name || "Usuario no identificado";
|
||||||
}
|
}
|
||||||
if (userProfileSubtitle) {
|
if (userProfileSubtitle) {
|
||||||
const email = firstProfileField(profile, ["email", "emailAddress", "email_address", "eMail", "mail"]);
|
const email = firstProfileField(profile, ["email", "emailAddress", "email_address", "eMail", "mail"]);
|
||||||
const phone = firstProfileField(profile, ["phone", "phoneNumber", "phone_number", "mobile", "telephone", "tel"]);
|
const phone = firstProfileField(profile, ["phone", "phoneNumber", "phone_number", "mobile", "telephone", "tel"]);
|
||||||
const parts = [email, phone].filter(Boolean);
|
const parts = [email, phone].filter(Boolean);
|
||||||
userProfileSubtitle.textContent = parts.length ? parts.join(" · ") : (name ? "Nessuna informazione di contatto disponibile." : "Connettersi a Vocero per vedere i dati utente.");
|
userProfileSubtitle.textContent = parts.length ? parts.join(" · ") : (name ? "No hay información de contacto disponible." : "Conéctate a Vocero para ver los datos del usuario.");
|
||||||
}
|
}
|
||||||
if (userProfileMetadata) {
|
if (userProfileMetadata) {
|
||||||
userProfileMetadata.innerHTML = "";
|
userProfileMetadata.innerHTML = "";
|
||||||
@@ -1239,8 +1239,8 @@ function syncVoceroHeaderUser() {
|
|||||||
// Show avatar with the 2-letter initials. Full name is reachable via hover
|
// Show avatar with the 2-letter initials. Full name is reachable via hover
|
||||||
// tooltip (title) and is used by screen readers via aria-label.
|
// tooltip (title) and is used by screen readers via aria-label.
|
||||||
identUserName.textContent = initials;
|
identUserName.textContent = initials;
|
||||||
identUserName.title = `Utente: ${name}`;
|
identUserName.title = `Usuario: ${name}`;
|
||||||
identUserName.setAttribute("aria-label", `Utente: ${name}`);
|
identUserName.setAttribute("aria-label", `Usuario: ${name}`);
|
||||||
} else {
|
} else {
|
||||||
identUserName.textContent = "";
|
identUserName.textContent = "";
|
||||||
identUserName.removeAttribute("title");
|
identUserName.removeAttribute("title");
|
||||||
@@ -1301,7 +1301,7 @@ function syncVoceroHeaderUser() {
|
|||||||
badge1.textContent = `${count} programados`;
|
badge1.textContent = `${count} programados`;
|
||||||
const badge2 = document.createElement("span");
|
const badge2 = document.createElement("span");
|
||||||
badge2.className = "reports-summary-badge";
|
badge2.className = "reports-summary-badge";
|
||||||
badge2.textContent = `Bloque LLM ${totalLen.toLocaleString()} chars`;
|
badge2.textContent = `Bloque LLM ${totalLen.toLocaleString()} caracteres`;
|
||||||
titleRow.appendChild(titleText);
|
titleRow.appendChild(titleText);
|
||||||
titleRow.appendChild(badge1);
|
titleRow.appendChild(badge1);
|
||||||
titleRow.appendChild(badge2);
|
titleRow.appendChild(badge2);
|
||||||
@@ -1907,7 +1907,7 @@ function _kickIdleTimer(reason = "activity") {
|
|||||||
idleCloseTimer = 0;
|
idleCloseTimer = 0;
|
||||||
// Friendly caption flash right before teardown moves us back to the idle
|
// Friendly caption flash right before teardown moves us back to the idle
|
||||||
// copy (so the user understands *why* the call ended).
|
// copy (so the user understands *why* the call ended).
|
||||||
setCaption("Chiuso per inattività", "muted");
|
setCaption("Sesión cerrada por inactividad", "muted");
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
await teardown();
|
await teardown();
|
||||||
@@ -1917,7 +1917,7 @@ function _kickIdleTimer(reason = "activity") {
|
|||||||
// teardown() calls setState("idle") which restores the normal idle
|
// teardown() calls setState("idle") which restores the normal idle
|
||||||
// caption. Leave a visible breadcrumb on the subcaption briefly so the
|
// caption. Leave a visible breadcrumb on the subcaption briefly so the
|
||||||
// user can tell what happened instead of wondering why the orb reset.
|
// user can tell what happened instead of wondering why the orb reset.
|
||||||
circleSubcaption.textContent = "Sessione chiusa dopo 30 secondi di silenzio. Tocca l'orb o ripeti «Hola Vocero» per iniziare di nuovo.";
|
circleSubcaption.textContent = "Sesión cerrada tras 30 segundos de silencio. Toca el círculo o repite «Hola Vocero» para empezar de nuevo.";
|
||||||
circleSubcaption.hidden = false;
|
circleSubcaption.hidden = false;
|
||||||
window.setTimeout(() => { circleSubcaption.hidden = true; }, 8000);
|
window.setTimeout(() => { circleSubcaption.hidden = true; }, 8000);
|
||||||
})();
|
})();
|
||||||
@@ -1968,7 +1968,7 @@ function setState(next) {
|
|||||||
// Warm reassurance under the terse position, only while waiting in line.
|
// Warm reassurance under the terse position, only while waiting in line.
|
||||||
if (next === "queued") {
|
if (next === "queued") {
|
||||||
circleSubcaption.textContent =
|
circleSubcaption.textContent =
|
||||||
"Sorry, we overhugged! 🤗 Every slot is busy, so we saved you a spot. Hang tight, you're moving up.";
|
"¡Nos pasamos de abrazos! 🤗 Todos los lugares están ocupados, pero guardamos uno para ti en la cola. Ya estás avanzando.";
|
||||||
circleSubcaption.hidden = false;
|
circleSubcaption.hidden = false;
|
||||||
} else {
|
} else {
|
||||||
circleSubcaption.hidden = true;
|
circleSubcaption.hidden = true;
|
||||||
@@ -2003,8 +2003,8 @@ function updateRestartAvailability() {
|
|||||||
currentState === "connecting" || currentState === "queued" || currentState === "your-turn";
|
currentState === "connecting" || currentState === "queued" || currentState === "your-turn";
|
||||||
restartHint.hidden = false;
|
restartHint.hidden = false;
|
||||||
restartHint.textContent = LIVE_STATES.has(currentState)
|
restartHint.textContent = LIVE_STATES.has(currentState)
|
||||||
? "Reconnects now with the settings above."
|
? "Reconecta ahora con la configuración indicada."
|
||||||
: "Starts a conversation with the settings above.";
|
: "Inicia una conversación con la configuración indicada.";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2031,7 +2031,7 @@ function openSettings() {
|
|||||||
inputInstructions.readOnly = !!pinnedPrompt;
|
inputInstructions.readOnly = !!pinnedPrompt;
|
||||||
primaryPromptMeta.hidden = !pinnedPrompt;
|
primaryPromptMeta.hidden = !pinnedPrompt;
|
||||||
primaryPromptMeta.textContent = pinnedPrompt
|
primaryPromptMeta.textContent = pinnedPrompt
|
||||||
? "Primary prompt pinned by this deployment."
|
? "Instrucciones principales fijadas por este despliegue."
|
||||||
: "";
|
: "";
|
||||||
syncGateUi();
|
syncGateUi();
|
||||||
updateRestartAvailability();
|
updateRestartAvailability();
|
||||||
@@ -2118,7 +2118,7 @@ function setGateThreshold(db) {
|
|||||||
settings.noiseGate = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(db)));
|
settings.noiseGate = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(db)));
|
||||||
const off = settings.noiseGate <= GATE_OFF_DB;
|
const off = settings.noiseGate <= GATE_OFF_DB;
|
||||||
inputNoiseGate.value = String(settings.noiseGate);
|
inputNoiseGate.value = String(settings.noiseGate);
|
||||||
gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`;
|
gateValue.textContent = off ? "Desactivada" : `${settings.noiseGate} dB`;
|
||||||
renderGateHandle();
|
renderGateHandle();
|
||||||
localStorage.setItem(STORAGE_KEYS.noiseGate, String(settings.noiseGate));
|
localStorage.setItem(STORAGE_KEYS.noiseGate, String(settings.noiseGate));
|
||||||
if (client && LIVE_STATES.has(currentState)) {
|
if (client && LIVE_STATES.has(currentState)) {
|
||||||
@@ -2130,7 +2130,7 @@ function setGateThreshold(db) {
|
|||||||
function syncGateUi() {
|
function syncGateUi() {
|
||||||
inputNoiseGate.value = String(settings.noiseGate);
|
inputNoiseGate.value = String(settings.noiseGate);
|
||||||
const off = settings.noiseGate <= GATE_OFF_DB;
|
const off = settings.noiseGate <= GATE_OFF_DB;
|
||||||
gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`;
|
gateValue.textContent = off ? "Desactivada" : `${settings.noiseGate} dB`;
|
||||||
renderGateHandle();
|
renderGateHandle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2340,8 +2340,8 @@ searchKeyInput.addEventListener("input", () => {
|
|||||||
pushToolsToSession();
|
pushToolsToSession();
|
||||||
}
|
}
|
||||||
toolWebHint.textContent = userSearchKey
|
toolWebHint.textContent = userSearchKey
|
||||||
? "Using your key — stored in this browser only."
|
? "Se usa tu clave, guardada solo en este navegador."
|
||||||
: "No server key configured. Add your own Serper key to enable web search.";
|
: "No hay una clave del servidor configurada. Añade tu clave de Serper para activar la búsqueda web.";
|
||||||
});
|
});
|
||||||
|
|
||||||
async function runTool(name, argsJson, callId) {
|
async function runTool(name, argsJson, callId) {
|
||||||
@@ -2936,7 +2936,7 @@ function connectionTarget() {
|
|||||||
}
|
}
|
||||||
const directUrl = buildDirectWsUrl(pinnedUrl || settings.directUrl);
|
const directUrl = buildDirectWsUrl(pinnedUrl || settings.directUrl);
|
||||||
if (!directUrl) {
|
if (!directUrl) {
|
||||||
throw new Error("Enter a speech-to-speech server URL in Settings.");
|
throw new Error("Introduce la URL del servidor de voz en Configuración.");
|
||||||
}
|
}
|
||||||
return { directUrl };
|
return { directUrl };
|
||||||
}
|
}
|
||||||
@@ -3022,16 +3022,16 @@ function syncConnectionUi() {
|
|||||||
inputLbUrl.value = pinnedUrl;
|
inputLbUrl.value = pinnedUrl;
|
||||||
inputLbUrl.readOnly = true;
|
inputLbUrl.readOnly = true;
|
||||||
connHint.classList.remove("error");
|
connHint.classList.remove("error");
|
||||||
connHint.textContent = "Speech-to-speech server URL pinned by this deployment.";
|
connHint.textContent = "URL del servidor de voz fijada por este despliegue.";
|
||||||
} else if (allowDirect) {
|
} else if (allowDirect) {
|
||||||
// Direct mode: the user sets their own s2s server URL.
|
// Direct mode: the user sets their own s2s server URL.
|
||||||
connField.hidden = false;
|
connField.hidden = false;
|
||||||
inputLbUrl.value = settings.directUrl;
|
inputLbUrl.value = settings.directUrl;
|
||||||
inputLbUrl.readOnly = false;
|
inputLbUrl.readOnly = false;
|
||||||
inputLbUrl.placeholder = "http://localhost:port";
|
inputLbUrl.placeholder = "http://localhost:8080";
|
||||||
connHint.classList.remove("error");
|
connHint.classList.remove("error");
|
||||||
connHint.textContent =
|
connHint.textContent =
|
||||||
"URL of your speech-to-speech server, e.g. http://localhost:8080 (the app adds /v1/realtime).";
|
"URL de tu servidor de voz, por ejemplo http://localhost:8080 (la aplicación añade /v1/realtime).";
|
||||||
} else {
|
} else {
|
||||||
// LB mode: the load balancer URL is deployment-owned — hide it entirely so
|
// LB mode: the load balancer URL is deployment-owned — hide it entirely so
|
||||||
// its address is never exposed in Settings.
|
// its address is never exposed in Settings.
|
||||||
@@ -3049,7 +3049,7 @@ function missingServerUrl() {
|
|||||||
function promptServerUrl() {
|
function promptServerUrl() {
|
||||||
if (settingsModal.open) syncConnectionUi();
|
if (settingsModal.open) syncConnectionUi();
|
||||||
else openSettings();
|
else openSettings();
|
||||||
connHint.textContent = "Set the speech-to-speech server URL to start.";
|
connHint.textContent = "Configura la URL del servidor de voz para empezar.";
|
||||||
connHint.classList.add("error");
|
connHint.classList.add("error");
|
||||||
inputLbUrl.focus();
|
inputLbUrl.focus();
|
||||||
}
|
}
|
||||||
@@ -3127,8 +3127,8 @@ async function handleStartError(err) {
|
|||||||
setState("error");
|
setState("error");
|
||||||
setCaption(
|
setCaption(
|
||||||
err.code === "join-expired"
|
err.code === "join-expired"
|
||||||
? "Your spot expired. Tap to rejoin."
|
? "Tu lugar ha caducado. Toca para volver a la cola."
|
||||||
: "That took a while. Tap to rejoin.",
|
: "La espera se ha alargado. Toca para volver a la cola.",
|
||||||
"error",
|
"error",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -3144,8 +3144,8 @@ micBtn.addEventListener("click", () => {
|
|||||||
}
|
}
|
||||||
client.setMuted(micMuted);
|
client.setMuted(micMuted);
|
||||||
micBtn.classList.toggle("muted", micMuted);
|
micBtn.classList.toggle("muted", micMuted);
|
||||||
micBtn.setAttribute("aria-label", micMuted ? "Unmute" : "Mute");
|
micBtn.setAttribute("aria-label", micMuted ? "Activar micrófono" : "Silenciar micrófono");
|
||||||
micBtn.title = micMuted ? "Unmute" : "Mute";
|
micBtn.title = micMuted ? "Activar micrófono" : "Silenciar micrófono";
|
||||||
});
|
});
|
||||||
|
|
||||||
stopBtn.addEventListener("click", async () => {
|
stopBtn.addEventListener("click", async () => {
|
||||||
@@ -3178,7 +3178,7 @@ async function primeMicPermission() {
|
|||||||
for (const track of s.getTracks()) track.stop();
|
for (const track of s.getTracks()) track.stop();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Microphone access denied${err instanceof Error ? `: ${err.message}` : ""}`,
|
`Acceso al micrófono denegado${err instanceof Error ? `: ${err.message}` : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3193,7 +3193,7 @@ async function acquireMicStream() {
|
|||||||
/** @param {number} position Update the queued caption ("You're #N in line"). */
|
/** @param {number} position Update the queued caption ("You're #N in line"). */
|
||||||
function onQueuePosition(position) {
|
function onQueuePosition(position) {
|
||||||
const n = Number(position) || 0;
|
const n = Number(position) || 0;
|
||||||
setCaption(n > 0 ? `You're #${n} in line` : "Finding you a spot…", "muted");
|
setCaption(n > 0 ? `Estás en el puesto ${n} de la cola` : "Buscando un lugar para ti…", "muted");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── "Your turn" join countdown ──────────────────────────────────────────────
|
// ── "Your turn" join countdown ──────────────────────────────────────────────
|
||||||
@@ -3206,14 +3206,14 @@ function startJoinCountdown(sec) {
|
|||||||
stopJoinCountdown();
|
stopJoinCountdown();
|
||||||
let left = Math.max(0, Math.floor(sec));
|
let left = Math.max(0, Math.floor(sec));
|
||||||
const paint = () => {
|
const paint = () => {
|
||||||
joinQueueBtn.textContent = left > 0 ? `Join now (${left}s)` : "Join now";
|
joinQueueBtn.textContent = left > 0 ? `Entrar ahora (${left}s)` : "Entrar ahora";
|
||||||
};
|
};
|
||||||
paint();
|
paint();
|
||||||
joinCountdownTimer = window.setInterval(() => {
|
joinCountdownTimer = window.setInterval(() => {
|
||||||
left -= 1;
|
left -= 1;
|
||||||
if (left <= 0) {
|
if (left <= 0) {
|
||||||
stopJoinCountdown();
|
stopJoinCountdown();
|
||||||
joinQueueBtn.textContent = "Join now";
|
joinQueueBtn.textContent = "Entrar ahora";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
paint();
|
paint();
|
||||||
@@ -3241,7 +3241,7 @@ async function doStart(audioContext = null) {
|
|||||||
chat.clear();
|
chat.clear();
|
||||||
chat.reset();
|
chat.reset();
|
||||||
setState("connecting");
|
setState("connecting");
|
||||||
setCaption("Asking for mic…", "muted");
|
setCaption("Solicitando acceso al micrófono…", "muted");
|
||||||
|
|
||||||
// Create + resume the AudioContext SYNCHRONOUSLY, still inside the gesture.
|
// Create + resume the AudioContext SYNCHRONOUSLY, still inside the gesture.
|
||||||
// iOS Safari only starts an AudioContext from a user gesture; if we waited
|
// iOS Safari only starts an AudioContext from a user gesture; if we waited
|
||||||
@@ -3644,7 +3644,7 @@ async function tryTalkFirstDirect() {
|
|||||||
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;"
|
"color:#2563eb;font-weight:700;background:#dbeafe;padding:3px 6px;border-radius:4px;"
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
setCaption("Preparing your session…", "muted");
|
setCaption("Preparando tu sesión…", "muted");
|
||||||
// Let _ensureAudioContext create the context. If it stays suspended, the
|
// Let _ensureAudioContext create the context. If it stays suspended, the
|
||||||
// client resumes it on the first audio or the next user click.
|
// client resumes it on the first audio or the next user click.
|
||||||
await doStart();
|
await doStart();
|
||||||
@@ -3691,7 +3691,7 @@ async function startTalkFromGesture() {
|
|||||||
// This user gesture permits AudioContext and getUserMedia to start.
|
// This user gesture permits AudioContext and getUserMedia to start.
|
||||||
console.info("[start-session] 🎤 start doStart() from unlocker gesture (TALK mode)");
|
console.info("[start-session] 🎤 start doStart() from unlocker gesture (TALK mode)");
|
||||||
const audioContext = createResumedAudioContext();
|
const audioContext = createResumedAudioContext();
|
||||||
setCaption("Preparing your session…", "muted");
|
setCaption("Preparando tu sesión…", "muted");
|
||||||
await doStart(audioContext);
|
await doStart(audioContext);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[start-session] ⛔ doStart() from unlocker failed", err);
|
console.error("[start-session] ⛔ doStart() from unlocker failed", err);
|
||||||
|
|||||||
+52
-52
@@ -3495,10 +3495,10 @@ async def apipilot_execute(request: Request, body: ApipilotExecuteRequest):
|
|||||||
@app.post("/api/reasoning")
|
@app.post("/api/reasoning")
|
||||||
async def reasoning(req: ReasoningRequest):
|
async def reasoning(req: ReasoningRequest):
|
||||||
if S2S_REASONING_TYPE == "PROXY":
|
if S2S_REASONING_TYPE == "PROXY":
|
||||||
raise HTTPException(status_code=409, detail="Reasoning endpoint is disabled in PROXY mode.")
|
raise HTTPException(status_code=409, detail="El servicio de razonamiento está deshabilitado en el modo PROXY.")
|
||||||
transcript = (req.transcript or "").strip()
|
transcript = (req.transcript or "").strip()
|
||||||
if not transcript:
|
if not transcript:
|
||||||
raise HTTPException(status_code=400, detail="transcript is required.")
|
raise HTTPException(status_code=400, detail="Se requiere el campo transcript.")
|
||||||
|
|
||||||
# ── S2S_INJECT_PROFILE: resolve podcast episode + caller profile + reports list + latest summary ──
|
# ── S2S_INJECT_PROFILE: resolve podcast episode + caller profile + reports list + latest summary ──
|
||||||
# (When S2S_INJECT_PROFILE=false this entire block is a no-op; user_context
|
# (When S2S_INJECT_PROFILE=false this entire block is a no-op; user_context
|
||||||
@@ -3516,7 +3516,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
needs_phone_authed = bool(VOCERO_USER_PHONE_AUTH_URL.strip())
|
needs_phone_authed = bool(VOCERO_USER_PHONE_AUTH_URL.strip())
|
||||||
# 2026-09-11 user fix: remove mass injection of scheduled reports.
|
# 2026-09-11 user fix: remove mass injection of scheduled reports.
|
||||||
# treats S2S_REPORT_LIST as "need to fetch reports" ONLY when BOTH the URL is
|
# treats S2S_REPORT_LIST as "need to fetch reports" ONLY when BOTH the URL is
|
||||||
# configured AND the new opt-in S2S_INJECT_REPORTS flag is true. When
|
# configured AND the new opt-in S2S_INJECT_REPORTS flag is true. When
|
||||||
# S2S_INJECT_REPORTS=false (default) needs_reports stays False here → we skip
|
# S2S_INJECT_REPORTS=false (default) needs_reports stays False here → we skip
|
||||||
@@ -3822,7 +3822,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
if not NODE_RED_REASONING_URL:
|
if not NODE_RED_REASONING_URL:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="S2S_REASONING_NODE_RED_URL is not configured. Set the env var to the Node-RED endpoint (e.g. http://localhost:1881).",
|
detail="S2S_REASONING_NODE_RED_URL no está configurado. Configure esta variable de entorno con la dirección de Node-RED (por ejemplo, http://localhost:1881).",
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"[INJECT-PROFILE] REASONING->NODE_RED body fields. instructionsLen=%d userCtxLen=%d phoneLen=%d episodeIdLen=%d",
|
"[INJECT-PROFILE] REASONING->NODE_RED body fields. instructionsLen=%d userCtxLen=%d phoneLen=%d episodeIdLen=%d",
|
||||||
@@ -3857,7 +3857,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("[EXT][NODE_RED] unreachable: %r", exc)
|
logger.warning("[EXT][NODE_RED] unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Node-RED reasoning engine is unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el motor de razonamiento de Node-RED.")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EXT][NODE_RED] reasoning RESP status=%s len=%dB",
|
"[EXT][NODE_RED] reasoning RESP status=%s len=%dB",
|
||||||
resp.status_code, len(resp.content or b""),
|
resp.status_code, len(resp.content or b""),
|
||||||
@@ -3866,23 +3866,23 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
logger.warning("[EXT][NODE_RED] failed %s: %s", resp.status_code, resp.text[:500])
|
logger.warning("[EXT][NODE_RED] failed %s: %s", resp.status_code, resp.text[:500])
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=502,
|
status_code=502,
|
||||||
detail=f"Node-RED reasoning failed (HTTP {resp.status_code}).",
|
detail=f"El razonamiento de Node-RED falló (HTTP {resp.status_code}).",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
payload = resp.json()
|
payload = resp.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("[EXT][NODE_RED] invalid JSON: %s", resp.text[:500])
|
logger.warning("[EXT][NODE_RED] invalid JSON: %s", resp.text[:500])
|
||||||
raise HTTPException(status_code=502, detail="Node-RED returned invalid JSON.")
|
raise HTTPException(status_code=502, detail="Node-RED devolvió un JSON no válido.")
|
||||||
answer = payload.get("answer") if isinstance(payload, dict) else None
|
answer = payload.get("answer") if isinstance(payload, dict) else None
|
||||||
log_only = bool(isinstance(payload, dict) and payload.get("log_only") is True)
|
log_only = bool(isinstance(payload, dict) and payload.get("log_only") is True)
|
||||||
if not isinstance(answer, str):
|
if not isinstance(answer, str):
|
||||||
logger.warning("[EXT][NODE_RED] invalid answer type: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
logger.warning("[EXT][NODE_RED] invalid answer type: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
||||||
raise HTTPException(status_code=502, detail="Node-RED returned invalid answer field.")
|
raise HTTPException(status_code=502, detail="Node-RED devolvió un campo answer no válido.")
|
||||||
answer_stripped = answer.strip()
|
answer_stripped = answer.strip()
|
||||||
# An empty answer is intentional in log-only mode; forward the flag.
|
# An empty answer is intentional in log-only mode; forward the flag.
|
||||||
if not answer_stripped and not log_only:
|
if not answer_stripped and not log_only:
|
||||||
logger.warning("[EXT][NODE_RED] empty answer and !log_only: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
logger.warning("[EXT][NODE_RED] empty answer and !log_only: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
||||||
raise HTTPException(status_code=502, detail="Node-RED returned no answer without log_only flag.")
|
raise HTTPException(status_code=502, detail="Node-RED no devolvió una respuesta ni la marca log_only.")
|
||||||
usage = payload.get("usage") if isinstance(payload, dict) else None
|
usage = payload.get("usage") if isinstance(payload, dict) else None
|
||||||
if isinstance(usage, dict):
|
if isinstance(usage, dict):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -3920,7 +3920,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
tool_name_p1,
|
tool_name_p1,
|
||||||
json.dumps(tool_args_p1 or {}, ensure_ascii=True)[:300],
|
json.dumps(tool_args_p1 or {}, ensure_ascii=True)[:300],
|
||||||
)
|
)
|
||||||
logger.info("[APIPILOT-CALL-NAME] USE=NODE_RED_PASS2_EXEC_HELPER name=%r → forward a _apipilot_execute_tool", tool_name_p1)
|
logger.info("[APIPILOT-CALL-NAME] USE=NODE_RED_PASS2_EXEC_HELPER name=%r → forward to _apipilot_execute_tool", tool_name_p1)
|
||||||
tool_env_p1 = await _apipilot_execute_tool(
|
tool_env_p1 = await _apipilot_execute_tool(
|
||||||
auth_header=apipilot_auth_header,
|
auth_header=apipilot_auth_header,
|
||||||
demo_session=None,
|
demo_session=None,
|
||||||
@@ -3951,7 +3951,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"[APIPILOT] REASONING PASS-2 execute name=%s success=%s httpResultLen=%d trigger seconda chiamata Node-RED",
|
"[APIPILOT] REASONING PASS-2 execute name=%s success=%s httpResultLen=%d trigger second Node-RED call",
|
||||||
tool_name_p1,
|
tool_name_p1,
|
||||||
str(not is_tool_err).lower(),
|
str(not is_tool_err).lower(),
|
||||||
len(tool_text_p1),
|
len(tool_text_p1),
|
||||||
@@ -4020,15 +4020,15 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("[EXT][NODE_RED PASS-2] unreachable: %r", exc)
|
logger.warning("[EXT][NODE_RED PASS-2] unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Node-RED reasoning engine is unreachable (pass 2).")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el motor de razonamiento de Node-RED (segunda pasada).")
|
||||||
if resp2.status_code != 200:
|
if resp2.status_code != 200:
|
||||||
logger.warning("[EXT][NODE_RED PASS-2] failed %s: %s", resp2.status_code, resp2.text[:500])
|
logger.warning("[EXT][NODE_RED PASS-2] failed %s: %s", resp2.status_code, resp2.text[:500])
|
||||||
raise HTTPException(status_code=502, detail=f"Node-RED reasoning pass 2 failed (HTTP {resp2.status_code}).")
|
raise HTTPException(status_code=502, detail=f"La segunda pasada del razonamiento de Node-RED falló (HTTP {resp2.status_code}).")
|
||||||
try:
|
try:
|
||||||
payload2 = resp2.json()
|
payload2 = resp2.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("[EXT][NODE_RED PASS-2] invalid JSON: %s", resp2.text[:500])
|
logger.warning("[EXT][NODE_RED PASS-2] invalid JSON: %s", resp2.text[:500])
|
||||||
raise HTTPException(status_code=502, detail="Node-RED returned invalid JSON (pass 2).")
|
raise HTTPException(status_code=502, detail="Node-RED devolvió un JSON no válido (segunda pasada).")
|
||||||
final_ans = payload2.get("answer") if isinstance(payload2, dict) else None
|
final_ans = payload2.get("answer") if isinstance(payload2, dict) else None
|
||||||
if not isinstance(final_ans, str):
|
if not isinstance(final_ans, str):
|
||||||
# Fallback: the first message and raw result.
|
# Fallback: the first message and raw result.
|
||||||
@@ -4058,7 +4058,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
# Mode B: OPENAI calls OpenAI Chat Completions directly.
|
# Mode B: OPENAI calls OpenAI Chat Completions directly.
|
||||||
# ────────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────────
|
||||||
if not OPENAI_API_KEY:
|
if not OPENAI_API_KEY:
|
||||||
raise HTTPException(status_code=503, detail="OPENAI_API_KEY is not configured.")
|
raise HTTPException(status_code=503, detail="OPENAI_API_KEY no está configurado.")
|
||||||
system_parts = []
|
system_parts = []
|
||||||
instructions = effective_instructions or ((req.instructions or "").strip())
|
instructions = effective_instructions or ((req.instructions or "").strip())
|
||||||
if instructions:
|
if instructions:
|
||||||
@@ -4114,19 +4114,19 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("OpenAI reasoning unreachable: %r", exc)
|
logger.warning("OpenAI reasoning unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="OpenAI reasoning unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de razonamiento de OpenAI.")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EXT][OPENAI] reasoning RESP status=%s len=%dB",
|
"[EXT][OPENAI] reasoning RESP status=%s len=%dB",
|
||||||
resp.status_code, len(resp.content or b""),
|
resp.status_code, len(resp.content or b""),
|
||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
logger.warning("OpenAI reasoning failed %s: %s", resp.status_code, resp.text[:400])
|
logger.warning("OpenAI reasoning failed %s: %s", resp.status_code, resp.text[:400])
|
||||||
raise HTTPException(status_code=502, detail=f"OpenAI reasoning failed ({resp.status_code}).")
|
raise HTTPException(status_code=502, detail=f"El razonamiento de OpenAI falló ({resp.status_code}).")
|
||||||
try:
|
try:
|
||||||
payload = resp.json()
|
payload = resp.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("OpenAI reasoning returned invalid JSON: %s", resp.text[:400])
|
logger.warning("OpenAI reasoning returned invalid JSON: %s", resp.text[:400])
|
||||||
raise HTTPException(status_code=502, detail="OpenAI reasoning returned invalid JSON.")
|
raise HTTPException(status_code=502, detail="El servicio de razonamiento de OpenAI devolvió un JSON no válido.")
|
||||||
answer = ""
|
answer = ""
|
||||||
choices = payload.get("choices")
|
choices = payload.get("choices")
|
||||||
if isinstance(choices, list) and choices:
|
if isinstance(choices, list) and choices:
|
||||||
@@ -4143,7 +4143,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
if not answer:
|
if not answer:
|
||||||
logger.warning("OpenAI reasoning returned empty answer: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
logger.warning("OpenAI reasoning returned empty answer: %s", json.dumps(payload, ensure_ascii=True)[:800])
|
||||||
raise HTTPException(status_code=502, detail="OpenAI reasoning returned no answer.")
|
raise HTTPException(status_code=502, detail="El servicio de razonamiento de OpenAI no devolvió una respuesta.")
|
||||||
logger.info("[Vocero REASONING] engine=OPENAI transcript=%s answer=%s", transcript[:300], answer[:500])
|
logger.info("[Vocero REASONING] engine=OPENAI transcript=%s answer=%s", transcript[:300], answer[:500])
|
||||||
# Capture the ApiPilot marker, execute its tool and run OpenAI pass 2.
|
# Capture the ApiPilot marker, execute its tool and run OpenAI pass 2.
|
||||||
tool_name_p1_oai, tool_args_p1_oai = _apipilot_parse_call_marker(answer)
|
tool_name_p1_oai, tool_args_p1_oai = _apipilot_parse_call_marker(answer)
|
||||||
@@ -4154,7 +4154,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
tool_name_p1_oai,
|
tool_name_p1_oai,
|
||||||
json.dumps(tool_args_p1_oai or {}, ensure_ascii=True)[:300],
|
json.dumps(tool_args_p1_oai or {}, ensure_ascii=True)[:300],
|
||||||
)
|
)
|
||||||
logger.info("[APIPILOT-CALL-NAME] USE=OPENAI_PASS2_EXEC_HELPER name=%r → forward a _apipilot_execute_tool", tool_name_p1_oai)
|
logger.info("[APIPILOT-CALL-NAME] USE=OPENAI_PASS2_EXEC_HELPER name=%r → forward to _apipilot_execute_tool", tool_name_p1_oai)
|
||||||
tool_env_oai_p1 = await _apipilot_execute_tool(
|
tool_env_oai_p1 = await _apipilot_execute_tool(
|
||||||
auth_header=apipilot_auth_header,
|
auth_header=apipilot_auth_header,
|
||||||
demo_session=None,
|
demo_session=None,
|
||||||
@@ -4185,7 +4185,7 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"[APIPILOT] REASONING PASS-2 OPENAI execute name=%s success=%s resultLen=%d → seconda chiamata chat/completions",
|
"[APIPILOT] REASONING PASS-2 OPENAI execute name=%s success=%s resultLen=%d → second chat/completions call",
|
||||||
tool_name_p1_oai,
|
tool_name_p1_oai,
|
||||||
str(not is_tool_err_oai).lower(),
|
str(not is_tool_err_oai).lower(),
|
||||||
len(tool_text_oai),
|
len(tool_text_oai),
|
||||||
@@ -4263,15 +4263,15 @@ async def reasoning(req: ReasoningRequest):
|
|||||||
)
|
)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("[EXT][OPENAI PASS-2] unreachable: %r", exc)
|
logger.warning("[EXT][OPENAI PASS-2] unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="OpenAI reasoning unreachable (pass 2).")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de razonamiento de OpenAI (segunda pasada).")
|
||||||
if resp_oai_2.status_code != 200:
|
if resp_oai_2.status_code != 200:
|
||||||
logger.warning("[EXT][OPENAI PASS-2] failed %s: %s", resp_oai_2.status_code, resp_oai_2.text[:400])
|
logger.warning("[EXT][OPENAI PASS-2] failed %s: %s", resp_oai_2.status_code, resp_oai_2.text[:400])
|
||||||
raise HTTPException(status_code=502, detail=f"OpenAI reasoning pass 2 failed ({resp_oai_2.status_code}).")
|
raise HTTPException(status_code=502, detail=f"La segunda pasada del razonamiento de OpenAI falló ({resp_oai_2.status_code}).")
|
||||||
try:
|
try:
|
||||||
payload_oai_2 = resp_oai_2.json()
|
payload_oai_2 = resp_oai_2.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("[EXT][OPENAI PASS-2] invalid JSON: %s", resp_oai_2.text[:400])
|
logger.warning("[EXT][OPENAI PASS-2] invalid JSON: %s", resp_oai_2.text[:400])
|
||||||
raise HTTPException(status_code=502, detail="OpenAI reasoning returned invalid JSON (pass 2).")
|
raise HTTPException(status_code=502, detail="El servicio de razonamiento de OpenAI devolvió un JSON no válido (segunda pasada).")
|
||||||
final_oai_ans = ""
|
final_oai_ans = ""
|
||||||
choices_oai_2 = payload_oai_2.get("choices")
|
choices_oai_2 = payload_oai_2.get("choices")
|
||||||
if isinstance(choices_oai_2, list) and choices_oai_2:
|
if isinstance(choices_oai_2, list) and choices_oai_2:
|
||||||
@@ -4346,7 +4346,7 @@ async def realtime_proxy(ws: WebSocket):
|
|||||||
if not S2S_PROXY_TARGET:
|
if not S2S_PROXY_TARGET:
|
||||||
logger.debug("[EXT][S2S-PROXY] client=%s rejected: S2S_PROXY_TARGET not configured", client_ip)
|
logger.debug("[EXT][S2S-PROXY] client=%s rejected: S2S_PROXY_TARGET not configured", client_ip)
|
||||||
await ws.accept()
|
await ws.accept()
|
||||||
await ws.close(code=1013, reason="Realtime proxy not configured.")
|
await ws.close(code=1013, reason="El proxy de tiempo real no está configurado.")
|
||||||
return
|
return
|
||||||
|
|
||||||
await ws.accept()
|
await ws.accept()
|
||||||
@@ -4753,7 +4753,7 @@ async def realtime_proxy(ws: WebSocket):
|
|||||||
logger.warning("Realtime proxy failed: %r", exc)
|
logger.warning("Realtime proxy failed: %r", exc)
|
||||||
logger.debug("[EXT][S2S-PROXY] client=%s ERROR %s", client_ip, type(exc).__name__)
|
logger.debug("[EXT][S2S-PROXY] client=%s ERROR %s", client_ip, type(exc).__name__)
|
||||||
try:
|
try:
|
||||||
await ws.close(code=1011, reason="Upstream realtime unavailable.")
|
await ws.close(code=1011, reason="El servicio de tiempo real no está disponible.")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
@@ -4783,11 +4783,11 @@ async def user_by_phone(phoneNumber: str):
|
|||||||
if not VOCERO_USER_PHONE_AUTH_URL:
|
if not VOCERO_USER_PHONE_AUTH_URL:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="VOCERO_USER_PHONE_AUTH_URL is not configured on this deploy.",
|
detail="VOCERO_USER_PHONE_AUTH_URL no está configurado en este despliegue.",
|
||||||
)
|
)
|
||||||
phone = (phoneNumber or "").strip()
|
phone = (phoneNumber or "").strip()
|
||||||
if not phone:
|
if not phone:
|
||||||
raise HTTPException(status_code=400, detail="phoneNumber is required.")
|
raise HTTPException(status_code=400, detail="Se requiere el campo phoneNumber.")
|
||||||
|
|
||||||
# 0) Login-by-phone (optional — only if S2S_LOGIN_BY_PHONE env is set).
|
# 0) Login-by-phone (optional — only if S2S_LOGIN_BY_PHONE env is set).
|
||||||
login_token = None
|
login_token = None
|
||||||
@@ -4809,7 +4809,7 @@ async def user_by_phone(phoneNumber: str):
|
|||||||
resp = await http.get(target_url, headers=headers or None)
|
resp = await http.get(target_url, headers=headers or None)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("[EXT][USER-BY-PHONE] unreachable: %r", exc)
|
logger.warning("[EXT][USER-BY-PHONE] unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Upstream user-info endpoint is unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de información del usuario.")
|
||||||
# Forward the upstream status + JSON verbatim; never rewrite the payload.
|
# Forward the upstream status + JSON verbatim; never rewrite the payload.
|
||||||
if resp.status_code >= 500:
|
if resp.status_code >= 500:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -4821,7 +4821,7 @@ async def user_by_phone(phoneNumber: str):
|
|||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=502,
|
status_code=502,
|
||||||
detail="Upstream user-info endpoint returned invalid JSON.",
|
detail="El servicio de información del usuario devolvió un JSON no válido.",
|
||||||
)
|
)
|
||||||
return JSONResponse(status_code=resp.status_code if resp.status_code != 204 else 200, content=payload)
|
return JSONResponse(status_code=resp.status_code if resp.status_code != 204 else 200, content=payload)
|
||||||
|
|
||||||
@@ -4870,14 +4870,14 @@ async def user_reports(phoneNumber: str | None = None):
|
|||||||
"hints": [],
|
"hints": [],
|
||||||
"entries": [],
|
"entries": [],
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
"reason": "S2S_INJECT_REPORTS is disabled on this deploy; no scheduled reports exposed to UI or LLM.",
|
"reason": "S2S_INJECT_REPORTS está deshabilitado en este despliegue; no se muestran reportes programados en la interfaz ni se envían al modelo.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
# ───────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────
|
||||||
if not S2S_REPORT_LIST:
|
if not S2S_REPORT_LIST:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail="S2S_REPORT_LIST is not configured on this deploy.",
|
detail="S2S_REPORT_LIST no está configurado en este despliegue.",
|
||||||
)
|
)
|
||||||
phone = (phoneNumber or "").strip() or None
|
phone = (phoneNumber or "").strip() or None
|
||||||
# 0) Login-by-phone if enabled. Bearer token used only if login succeeded
|
# 0) Login-by-phone if enabled. Bearer token used only if login succeeded
|
||||||
@@ -4897,7 +4897,7 @@ async def user_reports(phoneNumber: str | None = None):
|
|||||||
"hints": [],
|
"hints": [],
|
||||||
"entries": [],
|
"entries": [],
|
||||||
"skipped": True,
|
"skipped": True,
|
||||||
"reason": "empty list / upstream error / nothing to inject",
|
"reason": "Lista vacía, error del servicio de origen o ningún contenido disponible.",
|
||||||
})
|
})
|
||||||
# Rebuild hints + normalized entries with the exact filtered output
|
# Rebuild hints + normalized entries with the exact filtered output
|
||||||
items_list: list = reports_raw
|
items_list: list = reports_raw
|
||||||
@@ -4951,8 +4951,8 @@ async def user_reports(phoneNumber: str | None = None):
|
|||||||
if total_raw_ui > cap_ui:
|
if total_raw_ui > cap_ui:
|
||||||
filtered = filtered[:cap_ui]
|
filtered = filtered[:cap_ui]
|
||||||
truncated_ui_note = (
|
truncated_ui_note = (
|
||||||
f"Showing newest {cap_ui} of {total_raw_ui} upstream reports. "
|
f"Se muestran los {cap_ui} reportes más recientes de un total de {total_raw_ui}. "
|
||||||
f"Adjust S2S_REPORTS_QUICK_HINT_MAX env to raise/lower the window."
|
"Ajuste la variable de entorno S2S_REPORTS_QUICK_HINT_MAX para cambiar la cantidad."
|
||||||
)
|
)
|
||||||
import datetime as _dt
|
import datetime as _dt
|
||||||
# Deduplicate by label for the UI JSON response only.
|
# Deduplicate by label for the UI JSON response only.
|
||||||
@@ -5001,12 +5001,12 @@ async def user_reports(phoneNumber: str | None = None):
|
|||||||
date_piece = str(raw_d)[:32]
|
date_piece = str(raw_d)[:32]
|
||||||
if label_piece and date_piece:
|
if label_piece and date_piece:
|
||||||
hints_list.append(
|
hints_list.append(
|
||||||
f"• Report #{idx}: '{label_piece}' — scheduled on {date_piece}"
|
f"• Reporte #{idx}: '{label_piece}' — programado para {date_piece}"
|
||||||
)
|
)
|
||||||
elif label_piece:
|
elif label_piece:
|
||||||
hints_list.append(f"• Report #{idx}: '{label_piece}'")
|
hints_list.append(f"• Reporte #{idx}: '{label_piece}'")
|
||||||
elif date_piece:
|
elif date_piece:
|
||||||
hints_list.append(f"• Report #{idx}: (no label) — scheduled on {date_piece}")
|
hints_list.append(f"• Reporte #{idx}: (sin título) — programado para {date_piece}")
|
||||||
resp_body = {
|
resp_body = {
|
||||||
"count": len(entries_out),
|
"count": len(entries_out),
|
||||||
"rawCount": total_raw_ui,
|
"rawCount": total_raw_ui,
|
||||||
@@ -5069,13 +5069,13 @@ async def search(req: SearchRequest):
|
|||||||
the user brought their own (then theirs is used for this request only)."""
|
the user brought their own (then theirs is used for this request only)."""
|
||||||
query = (req.query or "").strip()
|
query = (req.query or "").strip()
|
||||||
if not query:
|
if not query:
|
||||||
raise HTTPException(status_code=400, detail="Empty query.")
|
raise HTTPException(status_code=400, detail="La consulta está vacía.")
|
||||||
|
|
||||||
key = (req.key or "").strip() or SERPER_KEY
|
key = (req.key or "").strip() or SERPER_KEY
|
||||||
has_user_key = bool((req.key or "").strip())
|
has_user_key = bool((req.key or "").strip())
|
||||||
if not key:
|
if not key:
|
||||||
# No server key and the user didn't supply one — search is unavailable.
|
# No server key and the user didn't supply one — search is unavailable.
|
||||||
raise HTTPException(status_code=503, detail="Search is not configured.")
|
raise HTTPException(status_code=503, detail="La búsqueda no está configurada.")
|
||||||
|
|
||||||
headers = {"X-API-KEY": key, "Content-Type": "application/json"}
|
headers = {"X-API-KEY": key, "Content-Type": "application/json"}
|
||||||
payload = {"q": query, "num": MAX_RESULTS}
|
payload = {"q": query, "num": MAX_RESULTS}
|
||||||
@@ -5088,7 +5088,7 @@ async def search(req: SearchRequest):
|
|||||||
resp = await http.post(SERPER_URL, headers=headers, json=payload)
|
resp = await http.post(SERPER_URL, headers=headers, json=payload)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("Serper unreachable: %r", exc)
|
logger.warning("Serper unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Search provider unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el proveedor de búsqueda.")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EXT][SERPER] search RESP status=%s len=%dB",
|
"[EXT][SERPER] search RESP status=%s len=%dB",
|
||||||
resp.status_code, len(resp.content or b""),
|
resp.status_code, len(resp.content or b""),
|
||||||
@@ -5104,7 +5104,7 @@ async def search(req: SearchRequest):
|
|||||||
msg = resp.json().get("message")
|
msg = resp.json().get("message")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
detail = f"Search provider error ({resp.status_code})"
|
detail = f"Error del proveedor de búsqueda ({resp.status_code})"
|
||||||
if msg:
|
if msg:
|
||||||
detail += f": {msg}"
|
detail += f": {msg}"
|
||||||
raise HTTPException(status_code=502, detail=detail)
|
raise HTTPException(status_code=502, detail=detail)
|
||||||
@@ -5149,7 +5149,7 @@ async def session(request: Request):
|
|||||||
if not LOAD_BALANCER_URL:
|
if not LOAD_BALANCER_URL:
|
||||||
# No LB configured — this deploy is direct-mode only; the browser should
|
# No LB configured — this deploy is direct-mode only; the browser should
|
||||||
# never call this. 404 so it's indistinguishable from a missing route.
|
# never call this. 404 so it's indistinguishable from a missing route.
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
|
|
||||||
tier, keys, set_cookie = auth.resolve_identity(request)
|
tier, keys, set_cookie = auth.resolve_identity(request)
|
||||||
# Metering runs only on the deployed Space; off-Space the LB still proxies but
|
# Metering runs only on the deployed Space; off-Space the LB still proxies but
|
||||||
@@ -5181,7 +5181,7 @@ async def session(request: Request):
|
|||||||
lb = await http.post(url, headers={"Content-Type": "application/json"}, content="{}")
|
lb = await http.post(url, headers={"Content-Type": "application/json"}, content="{}")
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("Load balancer unreachable: %r", exc)
|
logger.warning("Load balancer unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Speech service unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de voz.")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EXT][LB] session RESP status=%s len=%dB",
|
"[EXT][LB] session RESP status=%s len=%dB",
|
||||||
lb.status_code, len(lb.content or b""),
|
lb.status_code, len(lb.content or b""),
|
||||||
@@ -5202,7 +5202,7 @@ async def session(request: Request):
|
|||||||
# The LB's error body may name the reason (e.g. capacity); it carries no
|
# The LB's error body may name the reason (e.g. capacity); it carries no
|
||||||
# secret, so relay a trimmed copy.
|
# secret, so relay a trimmed copy.
|
||||||
logger.warning("Session handshake failed %s: %s", lb.status_code, lb.text[:300])
|
logger.warning("Session handshake failed %s: %s", lb.status_code, lb.text[:300])
|
||||||
raise HTTPException(status_code=502, detail=f"Session handshake failed ({lb.status_code}).")
|
raise HTTPException(status_code=502, detail=f"No se pudo establecer la sesión ({lb.status_code}).")
|
||||||
|
|
||||||
data = lb.json()
|
data = lb.json()
|
||||||
state = data.get("state", "?")
|
state = data.get("state", "?")
|
||||||
@@ -5236,7 +5236,7 @@ async def queue_status(queue_id: str, request: Request):
|
|||||||
daily budget at claim, since a multi-minute wait could have spent it elsewhere."""
|
daily budget at claim, since a multi-minute wait could have spent it elsewhere."""
|
||||||
client_ip = request.client.host if request.client else "?"
|
client_ip = request.client.host if request.client else "?"
|
||||||
if not LOAD_BALANCER_URL:
|
if not LOAD_BALANCER_URL:
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
|
|
||||||
tier, keys, set_cookie = auth.resolve_identity(request)
|
tier, keys, set_cookie = auth.resolve_identity(request)
|
||||||
tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None
|
tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None
|
||||||
@@ -5252,7 +5252,7 @@ async def queue_status(queue_id: str, request: Request):
|
|||||||
lb = await http.get(url)
|
lb = await http.get(url)
|
||||||
except httpx.RequestError as exc:
|
except httpx.RequestError as exc:
|
||||||
logger.warning("Load balancer unreachable: %r", exc)
|
logger.warning("Load balancer unreachable: %r", exc)
|
||||||
raise HTTPException(status_code=502, detail="Speech service unreachable.")
|
raise HTTPException(status_code=502, detail="No se puede conectar con el servicio de voz.")
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[EXT][LB] queue RESP status=%s len=%dB",
|
"[EXT][LB] queue RESP status=%s len=%dB",
|
||||||
lb.status_code, len(lb.content or b""),
|
lb.status_code, len(lb.content or b""),
|
||||||
@@ -5269,7 +5269,7 @@ async def queue_status(queue_id: str, request: Request):
|
|||||||
|
|
||||||
if lb.status_code != 200:
|
if lb.status_code != 200:
|
||||||
logger.warning("Queue poll failed %s: %s", lb.status_code, lb.text[:300])
|
logger.warning("Queue poll failed %s: %s", lb.status_code, lb.text[:300])
|
||||||
raise HTTPException(status_code=502, detail=f"Queue poll failed ({lb.status_code}).")
|
raise HTTPException(status_code=502, detail=f"No se pudo consultar la cola de espera ({lb.status_code}).")
|
||||||
|
|
||||||
data = lb.json()
|
data = lb.json()
|
||||||
state = data.get("state", "?")
|
state = data.get("state", "?")
|
||||||
@@ -5311,7 +5311,7 @@ async def queue_status(queue_id: str, request: Request):
|
|||||||
async def queue_leave(queue_id: str):
|
async def queue_leave(queue_id: str):
|
||||||
"""Leave the queue from the explicit 'Leave queue' button (a real fetch)."""
|
"""Leave the queue from the explicit 'Leave queue' button (a real fetch)."""
|
||||||
if not LOAD_BALANCER_URL:
|
if not LOAD_BALANCER_URL:
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
logger.debug("[EXT][LB] queue LEAVE (button) queueId=%s", queue_id[:16])
|
logger.debug("[EXT][LB] queue LEAVE (button) queueId=%s", queue_id[:16])
|
||||||
await _lb_leave(queue_id)
|
await _lb_leave(queue_id)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -5322,7 +5322,7 @@ async def queue_end(request: Request):
|
|||||||
"""Leave the queue on teardown/tab-close (navigator.sendBeacon, which can only
|
"""Leave the queue on teardown/tab-close (navigator.sendBeacon, which can only
|
||||||
POST). Body: { queueId }. Best-effort; the LB reaps the ticket on TTL anyway."""
|
POST). Body: { queueId }. Best-effort; the LB reaps the ticket on TTL anyway."""
|
||||||
if not LOAD_BALANCER_URL:
|
if not LOAD_BALANCER_URL:
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
qid = await _queue_id(request)
|
qid = await _queue_id(request)
|
||||||
if qid:
|
if qid:
|
||||||
logger.debug("[EXT][LB] queue LEAVE (teardown) queueId=%s", qid[:16])
|
logger.debug("[EXT][LB] queue LEAVE (teardown) queueId=%s", qid[:16])
|
||||||
@@ -6495,7 +6495,7 @@ async def session_heartbeat(request: Request):
|
|||||||
budget is spent — the client then tears down."""
|
budget is spent — the client then tears down."""
|
||||||
client_ip = request.client.host if request.client else "?"
|
client_ip = request.client.host if request.client else "?"
|
||||||
if not LIMITER_ENABLED:
|
if not LIMITER_ENABLED:
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
sid = await _session_id(request)
|
sid = await _session_id(request)
|
||||||
alive = bool(sid) and await asyncio.to_thread(limiter.heartbeat, sid)
|
alive = bool(sid) and await asyncio.to_thread(limiter.heartbeat, sid)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -6511,7 +6511,7 @@ async def session_end(request: Request):
|
|||||||
chunk. Sent via navigator.sendBeacon, so it must succeed without a response."""
|
chunk. Sent via navigator.sendBeacon, so it must succeed without a response."""
|
||||||
client_ip = request.client.host if request.client else "?"
|
client_ip = request.client.host if request.client else "?"
|
||||||
if not LIMITER_ENABLED:
|
if not LIMITER_ENABLED:
|
||||||
raise HTTPException(status_code=404, detail="Not found.")
|
raise HTTPException(status_code=404, detail="No encontrado.")
|
||||||
sid = await _session_id(request)
|
sid = await _session_id(request)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[INTERNAL] /api/session/end client=%s sessionId=%s",
|
"[INTERNAL] /api/session/end client=%s sessionId=%s",
|
||||||
|
|||||||
+19
-19
@@ -80,7 +80,7 @@ export class Account {
|
|||||||
if (!me.loggedIn) {
|
if (!me.loggedIn) {
|
||||||
// Signed-out: a sign-in pill (only when OAuth is actually available).
|
// Signed-out: a sign-in pill (only when OAuth is actually available).
|
||||||
if (me.auth && me.loginUrl) {
|
if (me.auth && me.loginUrl) {
|
||||||
this._root.innerHTML = `<a class="signin-pill" href="${escHtml(me.loginUrl)}" title="Sign in for more time"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg><span>Sign in</span></a>`;
|
this._root.innerHTML = `<a class="signin-pill" href="${escHtml(me.loginUrl)}" title="Inicia sesión para obtener más tiempo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg><span>Iniciar sesión</span></a>`;
|
||||||
} else {
|
} else {
|
||||||
this._root.innerHTML = "";
|
this._root.innerHTML = "";
|
||||||
this._root.hidden = true;
|
this._root.hidden = true;
|
||||||
@@ -97,28 +97,28 @@ export class Account {
|
|||||||
: `<span class="account-avatar account-avatar-fallback">${escHtml((me.username || "?")[0].toUpperCase())}</span>`;
|
: `<span class="account-avatar account-avatar-fallback">${escHtml((me.username || "?")[0].toUpperCase())}</span>`;
|
||||||
const remaining =
|
const remaining =
|
||||||
isUnlimited || me.remainingSec == null
|
isUnlimited || me.remainingSec == null
|
||||||
? "Unlimited"
|
? "Sin límite"
|
||||||
: `${fmt(me.remainingSec)} left today`;
|
: `${fmt(me.remainingSec)} restantes hoy`;
|
||||||
const tierLabel = isPro ? "PRO" : isUnlimited ? "Team" : "Free";
|
const tierLabel = isPro ? "PRO" : isUnlimited ? "Equipo" : "Gratis";
|
||||||
|
|
||||||
this._root.innerHTML = `
|
this._root.innerHTML = `
|
||||||
<button id="account-chip" class="account-chip" aria-haspopup="true" aria-expanded="false">
|
<button id="account-chip" class="account-chip" aria-haspopup="true" aria-expanded="false">
|
||||||
${avatar}
|
${avatar}
|
||||||
<span class="account-handle">${escHtml(me.username || "you")}</span>
|
<span class="account-handle">${escHtml(me.username || "tú")}</span>
|
||||||
${isPro
|
${isPro
|
||||||
? '<span class="account-pro">PRO</span>'
|
? '<span class="account-pro">PRO</span>'
|
||||||
: me.tier === "org"
|
: me.tier === "org"
|
||||||
? '<span class="account-pro account-team">TEAM</span>'
|
? '<span class="account-pro account-team">EQUIPO</span>'
|
||||||
: ""}
|
: ""}
|
||||||
</button>
|
</button>
|
||||||
<div id="account-pop" class="account-pop" hidden>
|
<div id="account-pop" class="account-pop" hidden>
|
||||||
<div class="account-pop-row account-pop-name">${escHtml(me.username || "you")}</div>
|
<div class="account-pop-row account-pop-name">${escHtml(me.username || "tú")}</div>
|
||||||
<div class="account-pop-row account-pop-meta">
|
<div class="account-pop-row account-pop-meta">
|
||||||
<span class="account-tier">${tierLabel}</span>
|
<span class="account-tier">${tierLabel}</span>
|
||||||
<span class="account-remaining">${escHtml(remaining)}</span>
|
<span class="account-remaining">${escHtml(remaining)}</span>
|
||||||
</div>
|
</div>
|
||||||
${isUnlimited ? "" : `<a class="account-pop-link" href="${PRO_URL}" target="_blank" rel="noopener">Upgrade to PRO</a>`}
|
${isUnlimited ? "" : `<a class="account-pop-link" href="${PRO_URL}" target="_blank" rel="noopener">Pasar a PRO</a>`}
|
||||||
<a class="account-pop-link account-signout" href="${escHtml(me.logoutUrl || "#")}">Sign out</a>
|
<a class="account-pop-link account-signout" href="${escHtml(me.logoutUrl || "#")}">Cerrar sesión</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const chip = $("#account-chip");
|
const chip = $("#account-chip");
|
||||||
@@ -152,13 +152,13 @@ export class Account {
|
|||||||
*/
|
*/
|
||||||
showLimit(tier = this.tier) {
|
showLimit(tier = this.tier) {
|
||||||
const canSignIn = this._me.auth && this._me.loginUrl;
|
const canSignIn = this._me.auth && this._me.loginUrl;
|
||||||
this._modalTitle.textContent = "Thanks for chatting!";
|
this._modalTitle.textContent = "¡Gracias por conversar!";
|
||||||
if (tier === "anon") {
|
if (tier === "anon") {
|
||||||
this._modalMsg.textContent =
|
this._modalMsg.textContent =
|
||||||
"Guest conversations run for 5 minutes. Sign in with Hugging Face to get 10 minutes a day for free, and PRO members chat with no limit at all.";
|
"Las conversaciones de invitados duran 5 minutos. Inicia sesión con Hugging Face para obtener 10 minutos gratis al día. Los miembros PRO pueden conversar sin límite.";
|
||||||
this._modalNote.textContent = "Your free minutes refresh tomorrow.";
|
this._modalNote.textContent = "Tus minutos gratis se renuevan mañana.";
|
||||||
if (canSignIn) {
|
if (canSignIn) {
|
||||||
this._modalCta.innerHTML = `${HF_MARK}<span>Sign in with Hugging Face</span>`;
|
this._modalCta.innerHTML = `${HF_MARK}<span>Iniciar sesión con Hugging Face</span>`;
|
||||||
this._modalCta.href = /** @type {string} */ (this._me.loginUrl);
|
this._modalCta.href = /** @type {string} */ (this._me.loginUrl);
|
||||||
this._modalCta.hidden = false;
|
this._modalCta.hidden = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -167,9 +167,9 @@ export class Account {
|
|||||||
} else {
|
} else {
|
||||||
// Signed-in, non-PRO.
|
// Signed-in, non-PRO.
|
||||||
this._modalMsg.textContent =
|
this._modalMsg.textContent =
|
||||||
"You've enjoyed your 10 minutes for today. Go PRO for unlimited conversations and to support open source AI.";
|
"Ya has usado tus 10 minutos de hoy. Pasa a PRO para conversar sin límite y apoyar la IA de código abierto.";
|
||||||
this._modalNote.textContent = "Or come back tomorrow. Your minutes reset daily.";
|
this._modalNote.textContent = "También puedes volver mañana. Tus minutos se renuevan cada día.";
|
||||||
this._modalCta.innerHTML = "<span>Upgrade to PRO</span>";
|
this._modalCta.innerHTML = "<span>Pasar a PRO</span>";
|
||||||
this._modalCta.href = PRO_URL;
|
this._modalCta.href = PRO_URL;
|
||||||
this._modalCta.hidden = false;
|
this._modalCta.hidden = false;
|
||||||
}
|
}
|
||||||
@@ -179,10 +179,10 @@ export class Account {
|
|||||||
/** Show a warm "we're at capacity" message when even the waiting line is full.
|
/** Show a warm "we're at capacity" message when even the waiting line is full.
|
||||||
* Reuses the limit modal shell; no call-to-action, just reassurance. */
|
* Reuses the limit modal shell; no call-to-action, just reassurance. */
|
||||||
showBusy() {
|
showBusy() {
|
||||||
this._modalTitle.textContent = "Hugged to the limit 🤗";
|
this._modalTitle.textContent = "¡Estamos al límite de abrazos! 🤗";
|
||||||
this._modalMsg.textContent =
|
this._modalMsg.textContent =
|
||||||
"Every slot and the whole line are full right now. Too much love! Grab a coffee and pop back in a minute.";
|
"Todos los lugares y la cola están ocupados. ¡Demasiado cariño! Tómate un café y vuelve en un minuto.";
|
||||||
this._modalNote.textContent = "A spot usually opens up within a few minutes.";
|
this._modalNote.textContent = "Normalmente se libera un lugar en pocos minutos.";
|
||||||
this._modalCta.hidden = true;
|
this._modalCta.hidden = true;
|
||||||
if (!this._modal.open) this._modal.showModal();
|
if (!this._modal.open) this._modal.showModal();
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-11
@@ -34,7 +34,7 @@ function reportChatDebug(hypothesisId, location, msg, data = {}) {
|
|||||||
|
|
||||||
const WRENCH_PATH = `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>`;
|
const WRENCH_PATH = `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>`;
|
||||||
const CHAT_BUBBLE_SVG = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
|
const CHAT_BUBBLE_SVG = `<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
|
||||||
const EMPTY_STATE_HTML = `<div id="chat-empty" class="chat-empty">${CHAT_BUBBLE_SVG}<span class="chat-empty-title">No messages yet</span><span class="chat-empty-hint">Tap the orb and start talking</span></div>`;
|
const EMPTY_STATE_HTML = `<div id="chat-empty" class="chat-empty">${CHAT_BUBBLE_SVG}<span class="chat-empty-title">Aún no hay mensajes</span><span class="chat-empty-hint">Toca el círculo y empieza a hablar</span></div>`;
|
||||||
/**
|
/**
|
||||||
* @param {string} prefix
|
* @param {string} prefix
|
||||||
* @param {string} [sep]
|
* @param {string} [sep]
|
||||||
@@ -298,7 +298,7 @@ export class ChatView {
|
|||||||
_buildMessageEl({ container, prefix, role, text, partial = false }) {
|
_buildMessageEl({ container, prefix, role, text, partial = false }) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = `${container} ${role}`;
|
el.className = `${container} ${role}`;
|
||||||
const label = role === "user" ? "You" : "Assistant";
|
const label = role === "user" ? "Tú" : "Asistente";
|
||||||
const stamp = this._nowStamp();
|
const stamp = this._nowStamp();
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
const uName = this._resolveUserName();
|
const uName = this._resolveUserName();
|
||||||
@@ -451,17 +451,17 @@ export class ChatView {
|
|||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "hist-msg tool";
|
el.className = "hist-msg tool";
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="hist-role">Tool call</div>
|
<div class="hist-role">Llamada a herramienta</div>
|
||||||
<button class="hist-tool-header" aria-expanded="false">
|
<button class="hist-tool-header" aria-expanded="false">
|
||||||
<svg class="hist-tool-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${WRENCH_PATH}</svg>
|
<svg class="hist-tool-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${WRENCH_PATH}</svg>
|
||||||
<span class="hist-tool-name">${escHtml(name)}</span>
|
<span class="hist-tool-name">${escHtml(name)}</span>
|
||||||
<svg class="hist-tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg class="hist-tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="hist-tool-body">
|
<div class="hist-tool-body">
|
||||||
<div class="hist-tool-label">Input</div>
|
<div class="hist-tool-label">Entrada</div>
|
||||||
<div class="hist-tool-block">${escHtml(pretty)}</div>
|
<div class="hist-tool-block">${escHtml(pretty)}</div>
|
||||||
<div class="hist-tool-label">Output</div>
|
<div class="hist-tool-label">Salida</div>
|
||||||
<div class="hist-tool-block hist-tool-output">${escHtml(output || "(no output)")}</div>
|
<div class="hist-tool-block hist-tool-output">${escHtml(output || "(sin salida)")}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const header = /** @type {HTMLButtonElement} */ (el.querySelector(".hist-tool-header"));
|
const header = /** @type {HTMLButtonElement} */ (el.querySelector(".hist-tool-header"));
|
||||||
@@ -482,7 +482,7 @@ export class ChatView {
|
|||||||
hist.classList.add("interrupted");
|
hist.classList.add("interrupted");
|
||||||
const note = document.createElement("div");
|
const note = document.createElement("div");
|
||||||
note.className = "hist-note";
|
note.className = "hist-note";
|
||||||
note.textContent = "Interrupted";
|
note.textContent = "Interrumpido";
|
||||||
hist.appendChild(note);
|
hist.appendChild(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ export class ChatView {
|
|||||||
if (empty) empty.remove();
|
if (empty) empty.remove();
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "hist-msg tool";
|
el.className = "hist-msg tool";
|
||||||
el.innerHTML = `<div class="hist-role">Snapshot</div><img class="hist-image" alt="Webcam snapshot sent to the model" />`;
|
el.innerHTML = `<div class="hist-role">Captura</div><img class="hist-image" alt="Captura de la cámara enviada al modelo" />`;
|
||||||
const img = /** @type {HTMLImageElement} */ (el.querySelector("img"));
|
const img = /** @type {HTMLImageElement} */ (el.querySelector("img"));
|
||||||
img.src = dataUrl;
|
img.src = dataUrl;
|
||||||
this._chatHistory.appendChild(el);
|
this._chatHistory.appendChild(el);
|
||||||
@@ -607,11 +607,11 @@ export class ChatView {
|
|||||||
lines.push(head);
|
lines.push(head);
|
||||||
const args = String(m.args || "").trim();
|
const args = String(m.args || "").trim();
|
||||||
const output = String(m.output || "").trim();
|
const output = String(m.output || "").trim();
|
||||||
if (args) { lines.push(" Input:"); lines.push(" " + args.split("\n").join("\n ")); }
|
if (args) { lines.push(" Entrada:"); lines.push(" " + args.split("\n").join("\n ")); }
|
||||||
if (output) { lines.push(" Output:"); lines.push(" " + output.split("\n").join("\n ")); }
|
if (output) { lines.push(" Salida:"); lines.push(" " + output.split("\n").join("\n ")); }
|
||||||
lines.push("");
|
lines.push("");
|
||||||
} else if (m.type === "image") {
|
} else if (m.type === "image") {
|
||||||
const head = [m.timestamp, "SNAPSHOT (imagen web-cámara enviada al modelo)"].filter(Boolean).join(" ");
|
const head = [m.timestamp, "CAPTURA (imagen de la cámara enviada al modelo)"].filter(Boolean).join(" ");
|
||||||
lines.push(head);
|
lines.push(head);
|
||||||
lines.push(" " + String(m.dataUrl || "").slice(0, 80) + (String(m.dataUrl || "").length > 80 ? "…" : ""));
|
lines.push(" " + String(m.dataUrl || "").slice(0, 80) + (String(m.dataUrl || "").length > 80 ? "…" : ""));
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|||||||
Reference in New Issue
Block a user