@omnivoice/page-agent 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Page Agent — fork `@omnivoice`
2
2
 
3
- > **Fork publicado como [`@omnivoice/*`](https://www.npmjs.com/org/omnivoice) (v0.1.0), a partir do
3
+ > **Fork publicado como [`@omnivoice/*`](https://www.npmjs.com/org/omnivoice) (v0.2.0), a partir do
4
4
  > [page-agent](https://github.com/alibaba/page-agent) upstream `v1.12.4` (commit `9eb6b66`), MIT.**
5
5
 
6
6
  ## O que este fork muda
@@ -27,7 +27,73 @@ lista o conjunto completo). As principais:
27
27
  | `@omnivoice/page-controller` | Operações de DOM e interação com elementos |
28
28
  | `@omnivoice/ui` | Componentes de UI (painel e i18n) |
29
29
 
30
- ## Uso
30
+ ## Widget central + voz (v0.2.0)
31
+
32
+ **Um único ponto de integração** para qualquer projeto: `mountWidget(config)` centraliza
33
+ agente + painel + voz + configuração num objeto só. O agente é criado **lazy** (a página não
34
+ paga startup até o widget abrir), e a voz (reconhecimento + síntese, pt-BR) usa a Web Speech API
35
+ nativa — degrada graciosamente se o navegador não tiver mic/voz.
36
+
37
+ ```js
38
+ import { mountWidget } from "@omnivoice/page-agent";
39
+
40
+ const w = mountWidget({
41
+ // config central do LLM (um lugar só)
42
+ model: "gpt-4o-mini",
43
+ baseURL: "https://api.exemplo.com/v1",
44
+ apiKey: "sk-...",
45
+ language: "pt-BR",
46
+ voice: { input: true, output: true, speakOnComplete: true }, // fala o resultado
47
+ });
48
+
49
+ // handle: w.run(task) · w.stop() · w.show()/hide()/toggle() · w.setConfig(patch) · w.unmount()
50
+ ```
51
+
52
+ Ou via CDN (IIFE) — desligue o auto-init do demo com `data-auto-init="false"`:
53
+
54
+ ```html
55
+ <script src="https://unpkg.com/@omnivoice/page-agent@0.2.0/dist/iife/page-agent.demo.js"
56
+ data-auto-init="false"></script>
57
+ <script>
58
+ const w = PageAgent.mountWidget({ model: "...", baseURL: "...", apiKey: "...",
59
+ voice: { input: true, output: true } });
60
+ </script>
61
+ ```
62
+
63
+ O widget injeta um **launcher** (botão flutuante, mobile-friendly, com safe-area) e um
64
+ **botão de microfone** ao lado do campo de tarefa. Sem assets externos, arquivo único e
65
+ cacheável via CDN versionada — pensado para mobile/desktop e rede limitada.
66
+
67
+ ### Gateway — a chave fica no servidor, não no front
68
+
69
+ Colocar `apiKey` no HTML/JS expõe a chave a qualquer um que abrir o devtools. O padrão: o front
70
+ fala com um **gateway do projeto** (mesma origem do site), que injeta a chave ao chamar o modelo
71
+ e **controla a config** (modelo, URL do provedor, voz) por servidor. Cada projeto roda o seu
72
+ gateway com a sua config.
73
+
74
+ ```bash
75
+ # No projeto (ex.: NoStopPageTest). A chave vive SÓ no ambiente do servidor.
76
+ AGENT_BASE_URL=https://api.openai.com/v1 \
77
+ AGENT_API_KEY=sk-… \
78
+ AGENT_MODEL=gpt-4o-mini \
79
+ AGENT_VOICE_OUTPUT=true \
80
+ npx omnivoice-agent-gateway # bin do pacote @omnivoice/page-agent
81
+ # → GET /config (config pública, SEM chave) · POST /v1/chat/completions (proxy p/ o provedor)
82
+ ```
83
+
84
+ ```js
85
+ // No front: só aponta pro gateway. Sem chave, sem modelo — o servidor decide.
86
+ PageAgent.mountWidget({
87
+ baseURL: "http://127.0.0.1:8790/v1",
88
+ configUrl: "http://127.0.0.1:8790/config",
89
+ });
90
+ ```
91
+
92
+ O gateway **força o modelo** configurado (o front não escolhe) e **descarta qualquer
93
+ `Authorization` vinda do client** — uma página comprometida não usa outra chave/conta.
94
+ Config por env **ou** `AGENT_CONFIG=agent.config.json`. Zero dependências (Node 18+).
95
+
96
+ ## Uso (classe)
31
97
 
32
98
  ```bash
33
99
  npm install @omnivoice/page-agent
@@ -40,7 +106,7 @@ import { PageAgent } from "@omnivoice/page-agent";
40
106
  Ou single-file via CDN (exposto como `window.PageAgent`):
41
107
 
42
108
  ```html
43
- <script src="https://unpkg.com/@omnivoice/page-agent@0.1.0/dist/iife/page-agent.demo.js"></script>
109
+ <script src="https://unpkg.com/@omnivoice/page-agent@0.2.0/dist/iife/page-agent.demo.js"></script>
44
110
  ```
45
111
 
46
112
  ## Como publicar
@@ -1,9 +1,14 @@
1
1
  import { AgentConfig } from '@omnivoice/core';
2
+ import { ExecutionResult } from '@omnivoice/core';
2
3
  import { PageAgentCore } from '@omnivoice/core';
3
4
  import { PageControllerConfig } from '@omnivoice/page-controller';
4
5
  import { Panel } from '@omnivoice/ui';
5
6
  import { PanelConfig } from '@omnivoice/ui';
6
7
 
8
+ export declare function createVoice(options?: VoiceOptions): Voice;
9
+
10
+ export declare function mountWidget(options?: WidgetOptions): Widget;
11
+
7
12
  export declare class PageAgent extends PageAgentCore {
8
13
  panel: Panel;
9
14
  constructor(config: PageAgentConfig);
@@ -11,6 +16,135 @@ export declare class PageAgent extends PageAgentCore {
11
16
 
12
17
  export declare type PageAgentConfig = AgentConfig & PageControllerConfig & Omit<PanelConfig, 'language'>;
13
18
 
19
+ export declare interface Voice {
20
+ /** O que o navegador realmente oferece. A UI usa isto para mostrar/ocultar controles. */
21
+ readonly supported: {
22
+ input: boolean;
23
+ output: boolean;
24
+ };
25
+ /**
26
+ * Escuta uma vez e resolve com a transcrição final. Rejeita em erro real; resolve com ""
27
+ * quando o usuário não fala nada ('no-speech') — não é falha, é "nada foi dito".
28
+ */
29
+ listen(): Promise<string>;
30
+ /** Interrompe a escuta em andamento. */
31
+ stopListening(): void;
32
+ /** Fala um texto. `interrupt` corta o que estiver sendo dito antes de começar. */
33
+ speak(text: string, opts?: {
34
+ interrupt?: boolean;
35
+ }): void;
36
+ /** Para a síntese em andamento. */
37
+ stopSpeaking(): void;
38
+ /** Observa transições de estado (a UI mostra o indicador de "ouvindo/falando"). */
39
+ onStatus(cb: (status: VoiceStatus) => void): () => void;
40
+ }
41
+
42
+ export declare interface VoiceOptions {
43
+ /** Idioma BCP-47 do reconhecimento e da síntese. @default 'pt-BR' */
44
+ lang?: string;
45
+ /** Habilita o reconhecimento de fala (botão de microfone). @default true */
46
+ input?: boolean;
47
+ /** Habilita a síntese de fala (agente fala o resultado). @default false */
48
+ output?: boolean;
49
+ /** URI de uma voz específica do speechSynthesis (deixa o módulo escolher quando ausente). */
50
+ voiceURI?: string;
51
+ /** Velocidade da síntese (0.1–10). @default 1 */
52
+ rate?: number;
53
+ /** Tom da síntese (0–2). @default 1 */
54
+ pitch?: number;
55
+ /** Trava de segurança para `listen()`: ms até rejeitar se o reconhecimento não terminar. @default 30000 */
56
+ listenTimeoutMs?: number;
57
+ }
58
+
59
+ /**
60
+ * FORK: módulo de voz do widget. Zero dependências — usa apenas a Web Speech API nativa do
61
+ * navegador (reconhecimento + síntese), o que mantém o bundle leve e funciona offline/sem rede
62
+ * além das chamadas do próprio agente.
63
+ *
64
+ * Filosofia (regras do laboratório):
65
+ * - Degrada sem quebrar: se o navegador não tem mic nem síntese, `supported` informa e os
66
+ * métodos viram no-op silenciosos — a UI decide esconder os botões, o agente segue funcionando.
67
+ * - Espera por condição, nunca por relógio: `listen()` resolve quando chega o resultado final;
68
+ * o timeout é só uma trava de segurança contra um reconhecimento que "prende" a UI.
69
+ * - Nada de rede própria: o reconhecimento/síntese são do navegador (Chrome/Edge/Safari enviam
70
+ * apenas o áudio necessário para o serviço de voz do SO; em rede limitada isso é mínimo).
71
+ */
72
+ declare type VoiceStatus = 'idle' | 'listening' | 'speaking';
73
+
74
+ export declare interface Widget {
75
+ /** Agente centralizado (mesma instância que o painel controla). `null` até a primeira abertura. */
76
+ readonly agent: PageAgent | null;
77
+ /** Voz centralizada (reconhecimento + síntese). `null` até a primeira abertura. */
78
+ readonly voice: Voice | null;
79
+ /** Posição do botão-lançador. */
80
+ readonly launcher: HTMLElement;
81
+ /** Garante config (busca o `configUrl`) + voz + agente prontos. Idempotente; devolve o agente. */
82
+ ready(): Promise<PageAgent>;
83
+ /** Executa uma tarefa programaticamente. */
84
+ run(task: string): Promise<ExecutionResult>;
85
+ /** Para a tarefa em andamento (o agente segue reutilizável). */
86
+ stop(): Promise<void>;
87
+ /** Abre o painel (propaga erro de config). `hide()`/`toggle()` para fechar/alternar. */
88
+ show(): Promise<void>;
89
+ hide(): void;
90
+ toggle(): void;
91
+ /** Atualiza config em runtime (recria o agente quando muda algo de LLM/agente). */
92
+ setConfig(patch: Partial<WidgetOptions>): void;
93
+ /** Remove launcher, painel e voz da página. */
94
+ unmount(): void;
95
+ }
96
+
97
+ export declare interface WidgetOptions {
98
+ /** Modelo. Ex.: "gpt-4o-mini". */
99
+ model?: string;
100
+ /** Endpoint OpenAI-compatible. */
101
+ baseURL?: string;
102
+ /**
103
+ * Chave da API. Só para uso local/sem gateway (ex.: laboratório). Em produção, prefira
104
+ * `configUrl` + gateway: a chave fica no servidor e não aparece no front-end.
105
+ */
106
+ apiKey?: string;
107
+ /**
108
+ * URL de uma config pública servida pelo gateway do projeto (ex.: "/api/agent/config").
109
+ * O widget busca model/idioma/passos/voz DE LÁ — o front nunca precisa da chave. O gateway
110
+ * é a autoridade: ele injeta a `apiKey` no servidor e força o modelo configurado.
111
+ */
112
+ configUrl?: string;
113
+ /** Customiza o corpo da request (providers específicos). */
114
+ transformRequestBody?: PageAgentConfig['transformRequestBody'];
115
+ /** `fetch` custom (proxy, credentials, baixa latência). */
116
+ customFetch?: PageAgentConfig['customFetch'];
117
+ maxRetries?: number;
118
+ /** Idioma da interface e do raciocínio. @default 'pt-BR' */
119
+ language?: PageAgentConfig['language'];
120
+ /** Máximo de passos por tarefa. @default 30 */
121
+ maxSteps?: number;
122
+ /** Instruções globais/por página. */
123
+ instructions?: PageAgentConfig['instructions'];
124
+ /** Ferramentas customizadas. */
125
+ customTools?: PageAgentConfig['customTools'];
126
+ /** Máscula sobre a página durante a execução. @default true */
127
+ enableMask?: boolean;
128
+ /** Expansão da viewport ao enumerar elementos. @default 0 (só o que está visível) */
129
+ viewportExpansion?: number;
130
+ /** Pergunta pela próxima tarefa ao concluir. @default true */
131
+ promptForNextTask?: boolean;
132
+ /** Posição do botão-lançador. @default 'bottom-right' */
133
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
134
+ /** Rótulo acessível do botão-lançador. @default 'Abrir assistente da página' */
135
+ launcherLabel?: string;
136
+ /** Começa com o painel aberto (em vez de só o botão). @default false */
137
+ open?: boolean;
138
+ voice?: WidgetVoiceOptions;
139
+ }
140
+
141
+ export declare interface WidgetVoiceOptions extends VoiceOptions {
142
+ /** Fala o resultado quando a tarefa termina (requer `output: true`). @default true */
143
+ speakOnComplete?: boolean;
144
+ /** Fala o próximo objetivo a cada passo (mais falante; útil para mãos-livres). @default false */
145
+ speakSteps?: boolean;
146
+ }
147
+
14
148
 
15
149
  export * from "@omnivoice/core";
16
150
 
@@ -1,7 +1,403 @@
1
+ (function() { try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); elementStyle.appendChild(document.createTextNode("/* FORK: estilos do widget central (launcher + microfone). Injetados via CSS-in-JS — sem\n request de CSS separada, sem fontes externas. Pensado para mobile (alvo de toque 48px,\n safe-area) e para não brigar com a página host (z-index alto, cor própria). */\n\n.page-agent-widget-launcher {\n position: fixed;\n z-index: 2147483630;\n width: 48px;\n height: 48px;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: #111827;\n color: #f9fafb;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);\n cursor: pointer;\n transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n/* Safe-area em telas com notch/barras: o botão não fica preso na borda. */\n.page-agent-widget-launcher {\n right: calc(20px + env(safe-area-inset-right, 0px));\n bottom: calc(20px + env(safe-area-inset-bottom, 0px));\n}\n\n.page-agent-widget-launcher:hover {\n transform: translateY(-1px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.34);\n background: #1f2937;\n}\n\n.page-agent-widget-launcher:active {\n transform: translateY(0) scale(0.97);\n}\n\n.page-agent-widget-launcher:focus-visible {\n outline: 3px solid #60a5fa;\n outline-offset: 2px;\n}\n\n.page-agent-widget-launcher svg {\n display: block;\n}\n\n/* Botão de microfone, injetado ao lado do input de tarefa do painel. */\n.page-agent-voice-mic {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n flex: 0 0 auto;\n margin-right: 8px;\n padding: 0;\n border: 1px solid rgba(148, 163, 184, 0.4);\n border-radius: 8px;\n background: rgba(148, 163, 184, 0.12);\n color: inherit;\n cursor: pointer;\n transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n.page-agent-voice-mic:hover {\n background: rgba(148, 163, 184, 0.22);\n border-color: rgba(148, 163, 184, 0.6);\n}\n\n.page-agent-voice-mic:active {\n transform: scale(0.96);\n}\n\n.page-agent-voice-mic:focus-visible {\n outline: 2px solid #60a5fa;\n outline-offset: 1px;\n}\n\n.page-agent-voice-mic.is-listening {\n background: rgba(239, 68, 68, 0.16);\n border-color: rgba(239, 68, 68, 0.6);\n animation: page-agent-voice-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes page-agent-voice-pulse {\n 0%,\n 100% {\n box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.35);\n }\n 50% {\n box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .page-agent-voice-mic.is-listening {\n animation: none;\n }\n}")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); }})();
1
2
  import { PageAgentCore } from "@omnivoice/core";
2
3
  import { PageController } from "@omnivoice/page-controller";
3
4
  import { Panel } from "@omnivoice/ui";
4
5
  export * from "@omnivoice/core";
6
+ //#region src/voice.ts
7
+ function getRecognitionCtor() {
8
+ const w = globalThis;
9
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
10
+ }
11
+ var STATUS_LISTENING = "listening";
12
+ var STATUS_SPEAKING = "speaking";
13
+ var STATUS_IDLE = "idle";
14
+ function createVoice(options = {}) {
15
+ const lang = options.lang ?? "pt-BR";
16
+ const rate = options.rate ?? 1;
17
+ const pitch = options.pitch ?? 1;
18
+ const inputEnabled = options.input ?? true;
19
+ const outputEnabled = options.output ?? false;
20
+ const listenTimeoutMs = options.listenTimeoutMs ?? 3e4;
21
+ const Recognition = inputEnabled ? getRecognitionCtor() : null;
22
+ const synth = typeof window !== "undefined" ? window.speechSynthesis : void 0;
23
+ const supported = {
24
+ input: Boolean(inputEnabled && Recognition),
25
+ output: Boolean(outputEnabled && synth)
26
+ };
27
+ let status = STATUS_IDLE;
28
+ const statusListeners = /* @__PURE__ */ new Set();
29
+ function setStatus(next) {
30
+ if (status === next) return;
31
+ status = next;
32
+ statusListeners.forEach((cb) => {
33
+ try {
34
+ cb(next);
35
+ } catch {}
36
+ });
37
+ }
38
+ let activeRecognition = null;
39
+ let currentVoice = null;
40
+ if (synth) {
41
+ const pickVoice = () => {
42
+ if (options.voiceURI) currentVoice = synth.getVoices().find((v) => v.voiceURI === options.voiceURI) ?? null;
43
+ else {
44
+ const voices = synth.getVoices();
45
+ const prefix = lang.split("-")[0].toLowerCase();
46
+ currentVoice = voices.find((v) => v.lang?.toLowerCase() === lang.toLowerCase()) ?? voices.find((v) => v.lang?.toLowerCase().startsWith(prefix)) ?? null;
47
+ }
48
+ };
49
+ pickVoice();
50
+ synth.onvoiceschanged = pickVoice;
51
+ }
52
+ function speak(text, opts) {
53
+ if (!synth || !outputEnabled) return;
54
+ const clean = (text ?? "").trim();
55
+ if (!clean) return;
56
+ if (opts?.interrupt !== false) synth.cancel();
57
+ const utterance = new SpeechSynthesisUtterance(clean);
58
+ utterance.lang = lang;
59
+ utterance.rate = rate;
60
+ utterance.pitch = pitch;
61
+ if (currentVoice) utterance.voice = currentVoice;
62
+ utterance.onend = () => setStatus(STATUS_IDLE);
63
+ utterance.onerror = () => setStatus(STATUS_IDLE);
64
+ setStatus(STATUS_SPEAKING);
65
+ synth.speak(utterance);
66
+ }
67
+ function listen() {
68
+ if (!Recognition) return Promise.reject(/* @__PURE__ */ new Error("Voz de entrada não suportada neste navegador"));
69
+ return new Promise((resolve, reject) => {
70
+ const recognition = new Recognition();
71
+ activeRecognition = recognition;
72
+ let finalText = "";
73
+ let settled = false;
74
+ let timer;
75
+ const finish = (fn) => {
76
+ if (settled) return;
77
+ settled = true;
78
+ if (timer) clearTimeout(timer);
79
+ if (activeRecognition === recognition) activeRecognition = null;
80
+ setStatus(STATUS_IDLE);
81
+ fn();
82
+ };
83
+ timer = setTimeout(() => {
84
+ try {
85
+ recognition.stop();
86
+ } catch {}
87
+ finish(() => reject(/* @__PURE__ */ new Error("Tempo de escuta esgotado")));
88
+ }, listenTimeoutMs);
89
+ recognition.lang = lang;
90
+ recognition.continuous = false;
91
+ recognition.interimResults = true;
92
+ recognition.maxAlternatives = 1;
93
+ recognition.onstart = () => setStatus(STATUS_LISTENING);
94
+ recognition.onresult = (event) => {
95
+ for (let i = event.resultIndex; i < event.results.length; i++) {
96
+ const result = event.results[i];
97
+ if (result?.isFinal) finalText = result[0]?.transcript ?? finalText;
98
+ }
99
+ };
100
+ recognition.onerror = (event) => {
101
+ const code = event?.error;
102
+ if (code === "no-speech" || code === "aborted") {
103
+ finish(() => resolve(finalText.trim()));
104
+ return;
105
+ }
106
+ finish(() => reject(/* @__PURE__ */ new Error(`Erro de reconhecimento de voz: ${code ?? "desconhecido"}`)));
107
+ };
108
+ recognition.onend = () => {
109
+ finish(() => resolve(finalText.trim()));
110
+ };
111
+ try {
112
+ recognition.start();
113
+ } catch (error) {
114
+ finish(() => reject(error instanceof Error ? error : new Error(String(error))));
115
+ }
116
+ });
117
+ }
118
+ return {
119
+ supported,
120
+ listen,
121
+ stopListening() {
122
+ try {
123
+ activeRecognition?.stop();
124
+ } catch {}
125
+ activeRecognition = null;
126
+ setStatus(STATUS_IDLE);
127
+ },
128
+ speak,
129
+ stopSpeaking() {
130
+ if (!synth) return;
131
+ synth.cancel();
132
+ setStatus(STATUS_IDLE);
133
+ },
134
+ onStatus(cb) {
135
+ statusListeners.add(cb);
136
+ return () => statusListeners.delete(cb);
137
+ }
138
+ };
139
+ }
140
+ //#endregion
141
+ //#region src/widget.ts
142
+ /**
143
+ * FORK: widget central do page-agent. É o ÚNICO ponto de integração para um projeto host:
144
+ * uma chamada `mountWidget(config)` centraliza agente + painel + voz + config, com defaults
145
+ * sensatos, criação lazy do agente (a página não paga startup até o widget abrir) e degradação
146
+ * graciosa de voz. Pensado para mobile/desktop e rede limitada/offline: arquivo único, sem
147
+ * assets externos, agente criado sob demanda.
148
+ *
149
+ * Uso em projeto host (npm):
150
+ * import { mountWidget } from "@omnivoice/page-agent";
151
+ * const w = mountWidget({ model, baseURL, apiKey, voice: { input: true, output: true } });
152
+ *
153
+ * Uso via CDN (IIFE):
154
+ * <script src=".../page-agent.demo.js" data-auto-init="false"><\/script>
155
+ * <script>PageAgent.mountWidget({ model, baseURL, apiKey })<\/script>
156
+ */
157
+ var DEFAULTS = {
158
+ language: "pt-BR",
159
+ maxSteps: 30,
160
+ enableMask: true,
161
+ viewportExpansion: 0,
162
+ promptForNextTask: true,
163
+ position: "bottom-right",
164
+ launcherLabel: "Abrir assistente da página",
165
+ open: false
166
+ };
167
+ var LAUNCHER_ICON = "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 3l1.9 4.6L18.5 9l-4.6 1.9L12 15.5l-1.9-4.6L5.5 9l4.6-1.4L12 3z\"/><path d=\"M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2z\"/></svg>";
168
+ var MIC_ICON = "<svg viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"9\" y=\"3\" width=\"6\" height=\"11\" rx=\"3\"/><path d=\"M5 11a7 7 0 0 0 14 0M12 18v3\"/></svg>";
169
+ function positionCss(position) {
170
+ switch (position) {
171
+ case "bottom-left": return {
172
+ bottom: "20px",
173
+ left: "20px",
174
+ right: "auto",
175
+ top: "auto"
176
+ };
177
+ case "top-right": return {
178
+ top: "20px",
179
+ right: "20px",
180
+ bottom: "auto",
181
+ left: "auto"
182
+ };
183
+ case "top-left": return {
184
+ top: "20px",
185
+ left: "20px",
186
+ bottom: "auto",
187
+ right: "auto"
188
+ };
189
+ default: return {
190
+ bottom: "20px",
191
+ right: "20px",
192
+ top: "auto",
193
+ left: "auto"
194
+ };
195
+ }
196
+ }
197
+ function mountWidget(options = {}) {
198
+ const position = options.position ?? DEFAULTS.position;
199
+ const launcherLabel = options.launcherLabel ?? DEFAULTS.launcherLabel;
200
+ const open = options.open ?? DEFAULTS.open;
201
+ let agent = null;
202
+ let voice = null;
203
+ let micButton = null;
204
+ let listening = false;
205
+ let configPromise = null;
206
+ async function ensureConfig() {
207
+ if (configPromise) return configPromise;
208
+ if (!options.configUrl) return;
209
+ configPromise = (async () => {
210
+ const res = await fetch(options.configUrl, { headers: { accept: "application/json" } });
211
+ if (!res.ok) throw new Error(`mountWidget: configUrl respondeu ${res.status} — o gateway está no ar?`);
212
+ const server = await res.json();
213
+ for (const key of [
214
+ "model",
215
+ "language",
216
+ "maxSteps",
217
+ "promptForNextTask",
218
+ "instructions",
219
+ "voice"
220
+ ]) {
221
+ const value = server[key];
222
+ if (value !== void 0 && value !== null) options[key] = value;
223
+ }
224
+ })();
225
+ return configPromise;
226
+ }
227
+ function ensureVoice() {
228
+ if (!voice) voice = createVoice(options.voice ?? {});
229
+ return voice;
230
+ }
231
+ async function ensureAgent() {
232
+ if (agent && !agent.disposed) return agent;
233
+ await ensureConfig();
234
+ const v = ensureVoice();
235
+ const model = options.model;
236
+ const baseURL = options.baseURL;
237
+ if (!model || !baseURL) throw new Error(options.configUrl ? "mountWidget: o gateway não enviou `model` e/ou falta `baseURL` no front. Confira AGENT_MODEL no gateway e a baseURL do widget." : "mountWidget: configure `model` e `baseURL` (config central do LLM) antes de abrir o agente.");
238
+ agent = new PageAgent({
239
+ model,
240
+ baseURL,
241
+ apiKey: options.apiKey,
242
+ transformRequestBody: options.transformRequestBody,
243
+ customFetch: options.customFetch,
244
+ maxRetries: options.maxRetries,
245
+ language: options.language ?? DEFAULTS.language,
246
+ maxSteps: options.maxSteps ?? DEFAULTS.maxSteps,
247
+ enableMask: options.enableMask ?? DEFAULTS.enableMask,
248
+ viewportExpansion: options.viewportExpansion ?? DEFAULTS.viewportExpansion,
249
+ instructions: options.instructions,
250
+ customTools: options.customTools,
251
+ promptForNextTask: options.promptForNextTask ?? DEFAULTS.promptForNextTask
252
+ });
253
+ wireVoiceOutput(agent, v);
254
+ injectMicButton(agent, v);
255
+ return agent;
256
+ }
257
+ function wireVoiceOutput(a, v) {
258
+ const speakOnComplete = options.voice?.speakOnComplete ?? true;
259
+ const speakSteps = options.voice?.speakSteps ?? false;
260
+ const onStatus = () => {
261
+ const status = a.status;
262
+ const result = a.lastResult;
263
+ if (status === "completed" || status === "error") {
264
+ if (speakOnComplete && v.supported.output) {
265
+ if (status === "error") v.speak("A tarefa encontrou um problema e foi parada.");
266
+ else if (result?.success) v.speak("Tarefa concluída com sucesso.");
267
+ else v.speak("A tarefa terminou, mas o resultado não foi confirmado.");
268
+ }
269
+ return;
270
+ }
271
+ if (speakSteps && status === "running" && v.supported.output) {
272
+ const goal = [...a.history].reverse().find((h) => h.type === "step")?.reflection?.next_goal;
273
+ if (goal) v.speak(goal, { interrupt: true });
274
+ }
275
+ };
276
+ a.addEventListener("statuschange", onStatus);
277
+ a.addEventListener("activity", (event) => {
278
+ const detail = event.detail;
279
+ if (detail?.type === "error" && detail.message && v.supported.output) v.speak(detail.message, { interrupt: true });
280
+ });
281
+ }
282
+ function injectMicButton(a, v) {
283
+ const wrapper = a.panel.wrapper;
284
+ const input = wrapper.querySelector("input");
285
+ if (!input || !v.supported.input) return;
286
+ if (wrapper.querySelector("[data-page-agent-voice=\"mic\"]")) return;
287
+ if (micButton) micButton.remove();
288
+ micButton = document.createElement("button");
289
+ micButton.type = "button";
290
+ micButton.className = "page-agent-voice-mic";
291
+ micButton.setAttribute("data-page-agent-voice", "mic");
292
+ micButton.setAttribute("aria-label", "Ditar tarefa por voz");
293
+ micButton.title = "Ditar tarefa por voz";
294
+ micButton.innerHTML = MIC_ICON;
295
+ micButton.style.display = "inline-flex";
296
+ (input.parentElement ?? wrapper).insertBefore(micButton, input);
297
+ v.onStatus((s) => {
298
+ micButton?.classList.toggle("is-listening", s === "listening");
299
+ });
300
+ micButton.addEventListener("click", async () => {
301
+ if (listening) {
302
+ v.stopListening();
303
+ return;
304
+ }
305
+ listening = true;
306
+ try {
307
+ const transcript = await v.listen();
308
+ if (transcript) {
309
+ input.value = transcript;
310
+ input.dispatchEvent(new KeyboardEvent("keydown", {
311
+ key: "Enter",
312
+ bubbles: true
313
+ }));
314
+ }
315
+ } catch (error) {
316
+ input.setAttribute("placeholder", error instanceof Error ? error.message : "Não foi possível ouvir; digite a tarefa.");
317
+ setTimeout(() => input.removeAttribute("placeholder"), 6e3);
318
+ } finally {
319
+ listening = false;
320
+ }
321
+ });
322
+ }
323
+ const launcher = document.createElement("button");
324
+ launcher.type = "button";
325
+ launcher.className = "page-agent-widget-launcher";
326
+ launcher.setAttribute("aria-label", launcherLabel);
327
+ launcher.title = launcherLabel;
328
+ launcher.innerHTML = LAUNCHER_ICON;
329
+ const pos = positionCss(position);
330
+ Object.assign(launcher.style, pos);
331
+ let panelVisible = false;
332
+ async function openPanel(silent = false) {
333
+ try {
334
+ (await ensureAgent()).panel.show();
335
+ panelVisible = true;
336
+ } catch (error) {
337
+ if (silent) console.error("[page-agent-widget] não abriu o agente:", error);
338
+ else throw error;
339
+ }
340
+ }
341
+ function closePanel() {
342
+ agent?.panel.hide?.();
343
+ panelVisible = false;
344
+ }
345
+ launcher.addEventListener("click", () => {
346
+ if (panelVisible) closePanel();
347
+ else openPanel(true);
348
+ });
349
+ document.body.appendChild(launcher);
350
+ if (open) openPanel(true);
351
+ return {
352
+ get agent() {
353
+ return agent;
354
+ },
355
+ get voice() {
356
+ return voice;
357
+ },
358
+ launcher,
359
+ ready() {
360
+ return ensureAgent();
361
+ },
362
+ async run(task) {
363
+ return await (await ensureAgent()).execute(task);
364
+ },
365
+ async stop() {
366
+ if (agent) await agent.stop();
367
+ },
368
+ show() {
369
+ return openPanel(false);
370
+ },
371
+ hide() {
372
+ closePanel();
373
+ },
374
+ toggle() {
375
+ launcher.click();
376
+ },
377
+ setConfig(patch) {
378
+ Object.assign(options, patch);
379
+ if (agent) {
380
+ agent.dispose();
381
+ agent = null;
382
+ }
383
+ if (panelVisible) {
384
+ panelVisible = false;
385
+ openPanel(true);
386
+ }
387
+ },
388
+ unmount() {
389
+ voice?.stopSpeaking();
390
+ voice?.stopListening();
391
+ agent?.dispose();
392
+ agent = null;
393
+ voice = null;
394
+ micButton?.remove();
395
+ micButton = null;
396
+ launcher.remove();
397
+ }
398
+ };
399
+ }
400
+ //#endregion
5
401
  //#region src/PageAgent.ts
6
402
  /**
7
403
  * Copyright (C) 2025 Alibaba Group Holding Limited
@@ -25,6 +421,6 @@ var PageAgent = class extends PageAgentCore {
25
421
  }
26
422
  };
27
423
  //#endregion
28
- export { PageAgent };
424
+ export { PageAgent, createVoice, mountWidget };
29
425
 
30
426
  //# sourceMappingURL=page-agent.js.map