@mosaicoo/svg-engine 0.1.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.
@@ -0,0 +1,459 @@
1
+ import{CommonModule as E}from"@angular/common";import*as n from"@angular/core";import{signal as s,Injectable as L,inject as f,computed as l,input as u,output as U,Injector as W,viewChild as K,ChangeDetectionStrategy as Q,Component as X}from"@angular/core";import*as D from"@angular/material/button";import{MatButtonModule as N}from"@angular/material/button";import{MatFormField as R,MatLabel as A,MatPrefix as P,MatSuffix as _}from"@angular/material/form-field";import{MatIcon as C}from"@angular/material/icon";import{MatInput as F}from"@angular/material/input";import*as y from"@angular/material/menu";import{MatMenuModule as V}from"@angular/material/menu";import*as Y from"@angular/material/progress-bar";import{MatProgressBarModule as j}from"@angular/material/progress-bar";import{MatTooltip as O}from"@angular/material/tooltip";import{VOICE_WHISPER_PROVIDER as J,NaturalLanguageService as Z,LlmIntentResolverService as ee,detectLanguage as te,tokenize as ne,isVagueForRuleBased as ae}from"@mosaicoo/svg-engine/ai/nlu";function q(){if(typeof window>"u")return null;const c=window;return c.SpeechRecognition??c.webkitSpeechRecognition??null}class m{isSupported=s(q()!==null).asReadonly();_listening=s(!1,...ngDevMode?[{debugName:"_listening"}]:[]);listening=this._listening.asReadonly();_lastError=s(null,...ngDevMode?[{debugName:"_lastError"}]:[]);lastError=this._lastError.asReadonly();activeRec=null;static DEFAULT_LISTEN_TIMEOUT_MS=3e4;static DEFAULT_SILENCE_MS=1e3;listen(e="pt-BR",t={}){const a=t.timeoutMs??m.DEFAULT_LISTEN_TIMEOUT_MS,o=t.silenceMs??m.DEFAULT_SILENCE_MS;return new Promise((i,g)=>{const w=q();if(w===null){g(new Error("Web Speech API not supported in this browser"));return}if(this.activeRec!==null){try{this.activeRec.abort()}catch{}this.activeRec=null}const r=new w;r.lang=e,r.continuous=!1,r.interimResults=!0,r.maxAlternatives=1,this._lastError.set(null),this.activeRec=r;let p=!1,k="",b=null;const I=()=>{b!==null&&(clearTimeout(b),b=null)},S=a>0?setTimeout(()=>{if(!p){p=!0,this._lastError.set("timeout");try{r.abort()}catch{}this.activeRec=null,this._listening.set(!1),g(new Error(`SpeechRecognition timeout after ${a}ms`))}},a):null,x=()=>{S!==null&&clearTimeout(S),I()},$=()=>{o<=0||(I(),b=setTimeout(()=>{try{r.stop()}catch{}},o))};r.onstart=()=>{this._listening.set(!0)},r.onresult=d=>{const T=Array.from(d.results).map(H=>H[0]?.transcript??"").join("");T.trim().length>0&&(k=T,$())},r.onerror=d=>{this._lastError.set(d.error??"unknown"),p||(p=!0,x(),g(new Error(`SpeechRecognition error: ${d.error}`)))},r.onend=()=>{this._listening.set(!1),this.activeRec===r&&(this.activeRec=null),p||(p=!0,x(),i(k.trim()))};try{r.start()}catch(d){x(),this.activeRec=null,this._listening.set(!1),g(d instanceof Error?d:new Error(String(d)))}})}stop(){if(this.activeRec!==null){try{this.activeRec.abort()}catch{}this.activeRec=null,this._listening.set(!1)}}static \u0275fac=n.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:m,deps:[],target:n.\u0275\u0275FactoryTarget.Injectable});static \u0275prov=n.\u0275\u0275ngDeclareInjectable({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:m,providedIn:"root"})}n.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:m,decorators:[{type:L,args:[{providedIn:"root"}]}]});const z="svge.voice.";function G(c){try{return typeof localStorage>"u"?null:localStorage.getItem(z+c)}catch{return null}}function B(c,e){try{if(typeof localStorage>"u")return;localStorage.setItem(z+c,e)}catch{}}class v{webSpeech=f(m);whisper=f(J,{optional:!0});_engine=s("web-speech",...ngDevMode?[{debugName:"_engine"}]:[]);engine=this._engine.asReadonly();whisperAvailable=l(()=>this.whisper!==null,...ngDevMode?[{debugName:"whisperAvailable"}]:[]);availableEngines=l(()=>{const e=[];this.webSpeech.isSupported()&&e.push("web-speech");const t=this.whisper;return t!==null&&t.isSupported()&&e.push("whisper"),e.length>1&&e.push("auto"),e},...ngDevMode?[{debugName:"availableEngines"}]:[]);isSupported=l(()=>this.availableEngines().length>0,...ngDevMode?[{debugName:"isSupported"}]:[]);listening=l(()=>this.webSpeech.listening()||(this.whisper?.listening()??!1),...ngDevMode?[{debugName:"listening"}]:[]);modelLoading=l(()=>this.whisper?.modelLoading?.()??!1,...ngDevMode?[{debugName:"modelLoading"}]:[]);_lastError=s(null,...ngDevMode?[{debugName:"_lastError"}]:[]);lastError=this._lastError.asReadonly();constructor(){const e=G("engine");if(e!==null&&this.availableEngines().includes(e)){this._engine.set(e);return}this.whisper!==null&&this._engine.set("auto")}setEngine(e){this.availableEngines().includes(e)&&(this._engine.set(e),B("engine",e))}async listen(e,t={}){this._lastError.set(null);const a=this._engine(),o=this.whisper;if(a==="whisper"&&o!==null)return this.runProvider(o,e,t);if(a==="web-speech"||o===null)return this.runProvider(this.webSpeech,e,t);try{return await this.webSpeech.listen(e,t)}catch(i){if(o.isSupported())try{return await o.listen(e,t)}catch{throw this._lastError.set(o.lastError()??"unknown"),i}throw this._lastError.set(this.webSpeech.lastError()??"unknown"),i}}stop(){this.webSpeech.stop(),this.whisper?.stop()}async runProvider(e,t,a){try{return await e.listen(t,a)}catch(o){throw this._lastError.set(e.lastError()??"unknown"),o}}static \u0275fac=n.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:v,deps:[],target:n.\u0275\u0275FactoryTarget.Injectable});static \u0275prov=n.\u0275\u0275ngDeclareInjectable({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:v,providedIn:"root"})}n.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:v,decorators:[{type:L,args:[{providedIn:"root"}]}],ctorParameters:()=>[]});const M=[{code:"pt-BR",label:"Portugu\xEAs (Brasil)",short:"PT-BR"},{code:"en-US",label:"English (US)",short:"EN"},{code:"es-ES",label:"Espa\xF1ol",short:"ES"}];function oe(c="pt-BR"){const t=(typeof navigator<"u"?navigator.language??"":"").toLowerCase();return t.startsWith("pt")?"pt-BR":t.startsWith("es")?"es-ES":t.startsWith("en")?"en-US":c}function ie(){const c=G("lang");return c!==null&&M.some(e=>e.code===c)?c:oe()}class h{label=u("Comando em linguagem natural",...ngDevMode?[{debugName:"label"}]:[]);placeholder=u("ex: criar ret\xE2ngulo vermelho 100x50",...ngDevMode?[{debugName:"placeholder"}]:[]);voiceLang=u("pt-BR",...ngDevMode?[{debugName:"voiceLang"}]:[]);autoDetectLanguage=u(!1,...ngDevMode?[{debugName:"autoDetectLanguage"}]:[]);parseDebounceMs=u(0,...ngDevMode?[{debugName:"parseDebounceMs"}]:[]);autoExecuteThreshold=u(.7,...ngDevMode?[{debugName:"autoExecuteThreshold"}]:[]);confirmGate=u(null,...ngDevMode?[{debugName:"confirmGate"}]:[]);enableLlmFallback=u(!0,...ngDevMode?[{debugName:"enableLlmFallback"}]:[]);llmModel=u(null,...ngDevMode?[{debugName:"llmModel"}]:[]);executed=U();nlu=f(Z);llm=f(ee);voice=f(v);hostInjector=f(W);textInputRef=K("textInput",...ngDevMode?[{debugName:"textInputRef"}]:[]);text=s("",...ngDevMode?[{debugName:"text"}]:[]);debouncedText=s("",...ngDevMode?[{debugName:"debouncedText"}]:[]);debounceHandle;lastResult=s(null,...ngDevMode?[{debugName:"lastResult"}]:[]);llmThinking=s(!1,...ngDevMode?[{debugName:"llmThinking"}]:[]);llmError=s(null,...ngDevMode?[{debugName:"llmError"}]:[]);llmMode=s("catalog",...ngDevMode?[{debugName:"llmMode"}]:[]);selectedModel=s(null,...ngDevMode?[{debugName:"selectedModel"}]:[]);availableModels=s([],...ngDevMode?[{debugName:"availableModels"}]:[]);modelOptions=l(()=>{const e=new Set,t=[],a=i=>{i.length>0&&!e.has(i)&&(e.add(i),t.push(i))};for(const i of this.availableModels())a(i);for(const i of this.llm.suggestedModels())a(i);const o=this.llm.defaultModel;return o!==null&&a(o),t},...ngDevMode?[{debugName:"modelOptions"}]:[]);effectiveModel=l(()=>this.selectedModel()??this.llmModel()??this.llm.defaultModel,...ngDevMode?[{debugName:"effectiveModel"}]:[]);constructor(){this.llm.isAvailable&&this.loadModels()}async loadModels(){try{const e=await this.llm.listModels();e.length>0&&this.availableModels.set(e)}catch{}}setLlmMode(e){this.llmMode.set(e)}selectModel(e){this.selectedModel.set(e)}requestModel(){return this.selectedModel()??this.llmModel()??void 0}candidates=l(()=>{const e=this.debouncedText().trim();return e.length===0?[]:(this.nlu.intents(),this.nlu.parse(e,{injector:this.hostInjector},{threshold:.25,maxResults:5}))},...ngDevMode?[{debugName:"candidates"}]:[]);detectedLanguage=l(()=>{const e=this.debouncedText().trim();return e.length===0?"unknown":te(ne(e)).language},...ngDevMode?[{debugName:"detectedLanguage"}]:[]);selectedLanguage=s(ie(),...ngDevMode?[{debugName:"selectedLanguage"}]:[]);languages=M;effectiveVoiceLang=l(()=>{if(this.autoDetectLanguage()){const e=this.detectedLanguage();if(e==="pt")return"pt-BR";if(e==="en")return"en-US"}return this.selectedLanguage()},...ngDevMode?[{debugName:"effectiveVoiceLang"}]:[]);topCandidate=l(()=>this.candidates()[0]??null,...ngDevMode?[{debugName:"topCandidate"}]:[]);alternatives=l(()=>this.candidates().slice(1),...ngDevMode?[{debugName:"alternatives"}]:[]);voiceErrorMessage=l(()=>{const e=this.voice.lastError();if(e===null||e==="aborted")return null;switch(e){case"network":return"Voz indispon\xEDvel: sem conex\xE3o com o servi\xE7o de reconhecimento (Google STT). Verifique internet, firewall corporativo ou extens\xF5es (uBlock/Brave Shields podem bloquear *.google.com).";case"not-allowed":case"service-not-allowed":return"Permiss\xE3o de microfone negada. Habilite no \xEDcone \u{1F512} da barra de endere\xE7o e tente novamente.";case"no-speech":return"N\xE3o detectei voz. Fale mais perto do microfone e tente de novo.";case"audio-capture":return"Microfone n\xE3o dispon\xEDvel. Verifique se h\xE1 um microfone conectado e se outra aba/app n\xE3o est\xE1 usando-o.";case"language-not-supported":return`Idioma "${this.voiceLang()}" n\xE3o suportado pelo navegador.`;case"bad-grammar":return"Erro de gram\xE1tica do reconhecimento \u2014 tente um comando mais simples.";case"not-supported":return"Voz local indispon\xEDvel neste ambiente (sem microfone ou WebAssembly).";case"load-failed":return"Falha ao carregar o modelo de voz local (Whisper). Verifique se os assets do modelo est\xE3o publicados (ex.: /assets/ml/whisper).";case"transcribe-failed":return"Falha ao transcrever o \xE1udio localmente. Fale novamente e tente de novo.";default:return`Erro de voz: ${e}`}},...ngDevMode?[{debugName:"voiceErrorMessage"}]:[]);setLanguage(e){this.selectedLanguage.set(e),B("lang",e)}languageShort(e){return M.find(t=>t.code===e)?.short??e}engineLabel(e){switch(e){case"web-speech":return"Navegador (r\xE1pido)";case"whisper":return"Local / Whisper (offline)";case"auto":return"Autom\xE1tico (ambos)"}}engineIcon(e){switch(e){case"web-speech":return"cloud";case"whisper":return"offline_bolt";case"auto":return"auto_awesome"}}micTooltip(){return this.voice.modelLoading()?"Carregando modelo de voz local\u2026":this.voice.listening()?"Parar":`Voz (${this.effectiveVoiceLang()})`}onInput(e){this.text.set(e),this.debounceHandle!==void 0&&(clearTimeout(this.debounceHandle),this.debounceHandle=void 0);const t=this.parseDebounceMs();if(t<=0){this.debouncedText.set(e);return}this.debounceHandle=setTimeout(()=>{this.debouncedText.set(e),this.debounceHandle=void 0},t)}setTextProgrammatically(e){this.text.set(e),this.debouncedText.set(e),this.debounceHandle!==void 0&&(clearTimeout(this.debounceHandle),this.debounceHandle=void 0);const t=this.textInputRef()?.nativeElement;t&&(t.value=e)}async runNow(){const e=this.text().trim();if(e.length===0)return;if(this.llmError.set(null),this.enableLlmFallback()&&this.llm.isAvailable&&ae(e)){await this.escalateToLlm(e);return}const t=await this.nlu.executeSequence(e,{injector:this.hostInjector},this.executeOptions());for(const o of t)this.executed.emit(o);if(this.lastResult.set(t[t.length-1]??null),t.some(o=>o.executed)){this.setTextProgrammatically("");return}const a=t.length>0&&t.every(o=>o.rejection==="no-match");this.enableLlmFallback()&&this.llm.isAvailable&&a&&await this.escalateToLlm(e)}async escalateToLlm(e){this.llmThinking.set(!0),this.llmError.set(null);try{if(this.llmMode()==="raw-svg"){await this.escalateRawSvg(e);return}const t=await this.llm.resolveAndExecute(e,{injector:this.hostInjector},{model:this.requestModel(),confirmGate:this.executeOptions().confirmGate});for(const a of t)this.executed.emit(a);t.length>0&&this.lastResult.set(t[t.length-1]),t.length===0?this.llmError.set("A IA n\xE3o encontrou comandos aplic\xE1veis para esse pedido."):t.some(a=>a.executed)&&this.setTextProgrammatically("")}catch{this.llmError.set("A IA n\xE3o conseguiu interpretar o pedido. Tente reformular.")}finally{this.llmThinking.set(!1)}}async escalateRawSvg(e){const t=await this.llm.generateAndInsertSvg(e,{injector:this.hostInjector},{model:this.requestModel()});t.ok?this.setTextProgrammatically(""):this.llmError.set(t.error??"A IA n\xE3o conseguiu gerar o SVG. Tente reformular.")}async askAi(){const e=this.text().trim();e.length===0||!this.llm.isAvailable||await this.escalateToLlm(e)}async execCandidate(e){const t=await this.nlu.executeCandidate(e,{injector:this.hostInjector},this.executeOptions());this.lastResult.set(t),this.executed.emit(t),t.executed&&this.setTextProgrammatically("")}async forceExecute(e){const t=e.candidate;if(t===null)return;const a=await this.nlu.executeCandidate(t,{injector:this.hostInjector},{...this.executeOptions(),confirmGate:async()=>!0});this.lastResult.set(a),this.executed.emit(a),a.executed&&this.setTextProgrammatically("")}canForceExecute(e){return e.executed||e.candidate===null?!1:e.rejection==="destructive-no-gate"||e.rejection==="below-threshold"}async toggleVoice(){if(this.voice.listening()){this.voice.stop();return}try{const e=await this.voice.listen(this.effectiveVoiceLang());e.length>0&&(this.setTextProgrammatically(e),await this.runNow())}catch{}}describeIntent(e){const t=e.intent.description;if(typeof t=="string"&&t.length>0)return t;const a=e.intent.keywords;return Array.isArray(a)&&a.length>0?a.slice(0,3).join(" / "):e.intent.id}confidencePercent(e){return Math.round(e.confidence*100)}describeRejection(e){if(e.candidate===null)return"Nenhum comando reconhecido";const t=this.describeIntent(e.candidate),a=this.confidencePercent(e.candidate);switch(e.rejection){case"below-threshold":return`Confidence baixa (${a}%) em "${t}" \u2014 confirme se \xE9 o que quer`;case"destructive-no-gate":return`A\xE7\xE3o destrutiva: "${t}" \u2014 confirme pra executar`;case"confirmation-declined":return`"${t}" cancelado`;case"no-match":return"Nenhum comando reconhecido";default:return`Rejeitado: ${t}`}}executeOptions(){const t=this.confirmGate()??(a=>{if(typeof window>"u"||typeof window.confirm!="function")return!0;const o=this.describeIntent(a),i=this.confidencePercent(a),g=a.intent.destructive?"[a\xE7\xE3o destrutiva] ":"";return window.confirm(`${g}Executar "${o}" (${i}%)?`)});return{autoExecuteThreshold:this.autoExecuteThreshold(),confirmGate:t}}static \u0275fac=n.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:h,deps:[],target:n.\u0275\u0275FactoryTarget.Component});static \u0275cmp=n.\u0275\u0275ngDeclareComponent({minVersion:"17.0.0",version:"21.2.13",type:h,isStandalone:!0,selector:"svge-nlu-input",inputs:{label:{classPropertyName:"label",publicName:"label",isSignal:!0,isRequired:!1,transformFunction:null},placeholder:{classPropertyName:"placeholder",publicName:"placeholder",isSignal:!0,isRequired:!1,transformFunction:null},voiceLang:{classPropertyName:"voiceLang",publicName:"voiceLang",isSignal:!0,isRequired:!1,transformFunction:null},autoDetectLanguage:{classPropertyName:"autoDetectLanguage",publicName:"autoDetectLanguage",isSignal:!0,isRequired:!1,transformFunction:null},parseDebounceMs:{classPropertyName:"parseDebounceMs",publicName:"parseDebounceMs",isSignal:!0,isRequired:!1,transformFunction:null},autoExecuteThreshold:{classPropertyName:"autoExecuteThreshold",publicName:"autoExecuteThreshold",isSignal:!0,isRequired:!1,transformFunction:null},confirmGate:{classPropertyName:"confirmGate",publicName:"confirmGate",isSignal:!0,isRequired:!1,transformFunction:null},enableLlmFallback:{classPropertyName:"enableLlmFallback",publicName:"enableLlmFallback",isSignal:!0,isRequired:!1,transformFunction:null},llmModel:{classPropertyName:"llmModel",publicName:"llmModel",isSignal:!0,isRequired:!1,transformFunction:null}},outputs:{executed:"executed"},viewQueries:[{propertyName:"textInputRef",first:!0,predicate:["textInput"],descendants:!0,isSignal:!0}],ngImport:n,template:`
2
+ <div class="svge-nlu-root">
3
+ <mat-form-field appearance="outline" class="svge-nlu-field">
4
+ <mat-label>{{ label() }}</mat-label>
5
+ <mat-icon matPrefix class="svge-nlu-prefix" aria-hidden="true">smart_toy</mat-icon>
6
+ <input
7
+ #textInput
8
+ matInput
9
+ type="text"
10
+ (input)="onInput($any($event.target).value)"
11
+ (keydown.enter)="runNow()"
12
+ [attr.aria-label]="label()"
13
+ [attr.aria-describedby]="topCandidate() ? 'svge-nlu-hint' : null"
14
+ [placeholder]="placeholder()"
15
+ />
16
+ <!--
17
+ Grupo de a\xE7\xF5es \xDANICO (matSuffix) \u2014 idioma + motor de voz +
18
+ microfone + run ficam LADO A LADO \xE0 direita do input. Antes
19
+ cada bot\xE3o era um matSuffix separado dentro de um bloco
20
+ condicional, e o form-field MDC posicionava os condicionais
21
+ errado (idioma/motor ca\xEDam \xE0 esquerda/embaixo). Um matSuffix
22
+ SEMPRE presente (flex) resolve: o Material s\xF3 ancora o
23
+ cont\xEAiner; os condicionais internos viram DOM comum. Os
24
+ mat-menu ficam junto de seus bot\xF5es (mesmo bloco) para
25
+ preservar o escopo do template ref.
26
+ -->
27
+ <span matSuffix class="svge-nlu-actions">
28
+ @if (voice.isSupported()) {
29
+ <button
30
+ mat-icon-button
31
+ type="button"
32
+ class="svge-nlu-lang"
33
+ [matMenuTriggerFor]="langMenu"
34
+ [matTooltip]="'Idioma da voz: ' + languageShort(selectedLanguage())"
35
+ aria-label="Selecionar idioma da voz"
36
+ >
37
+ <mat-icon>translate</mat-icon>
38
+ </button>
39
+ <mat-menu #langMenu="matMenu">
40
+ @for (l of languages; track l.code) {
41
+ <button mat-menu-item type="button" (click)="setLanguage(l.code)">
42
+ <mat-icon>{{ selectedLanguage() === l.code ? 'check' : 'language' }}</mat-icon>
43
+ <span>{{ l.label }}</span>
44
+ </button>
45
+ }
46
+ </mat-menu>
47
+ }
48
+ @if (voice.availableEngines().length > 1) {
49
+ <button
50
+ mat-icon-button
51
+ type="button"
52
+ class="svge-nlu-engine"
53
+ [matMenuTriggerFor]="engineMenu"
54
+ [matTooltip]="'Motor de voz: ' + engineLabel(voice.engine())"
55
+ aria-label="Selecionar motor de voz"
56
+ >
57
+ <mat-icon>{{ engineIcon(voice.engine()) }}</mat-icon>
58
+ </button>
59
+ <mat-menu #engineMenu="matMenu">
60
+ @for (e of voice.availableEngines(); track e) {
61
+ <button mat-menu-item type="button" (click)="voice.setEngine(e)">
62
+ <mat-icon>{{ voice.engine() === e ? 'check' : engineIcon(e) }}</mat-icon>
63
+ <span>{{ engineLabel(e) }}</span>
64
+ </button>
65
+ }
66
+ </mat-menu>
67
+ }
68
+ @if (voice.isSupported()) {
69
+ <button
70
+ mat-icon-button
71
+ type="button"
72
+ class="svge-nlu-mic"
73
+ [class.recording]="voice.listening()"
74
+ [disabled]="voice.modelLoading()"
75
+ [attr.aria-pressed]="voice.listening()"
76
+ [matTooltip]="micTooltip()"
77
+ (click)="toggleVoice()"
78
+ >
79
+ <mat-icon>{{
80
+ voice.modelLoading() ? 'hourglass_empty' : voice.listening() ? 'mic_off' : 'mic'
81
+ }}</mat-icon>
82
+ </button>
83
+ }
84
+ @if (llm.isAvailable) {
85
+ <button
86
+ mat-icon-button
87
+ type="button"
88
+ class="svge-nlu-ai-config"
89
+ [matMenuTriggerFor]="aiMenu"
90
+ [matTooltip]="
91
+ 'IA: ' +
92
+ (llmMode() === 'raw-svg' ? 'SVG livre' : 'com cat\xE1logo') +
93
+ ' \xB7 modelo ' +
94
+ (effectiveModel() ?? 'padr\xE3o')
95
+ "
96
+ aria-label="Configurar IA (modo e modelo)"
97
+ >
98
+ <mat-icon>tune</mat-icon>
99
+ </button>
100
+ <mat-menu #aiMenu="matMenu" class="svge-nlu-ai-menu">
101
+ <div class="svge-nlu-menu-title">Modo de gera\xE7\xE3o</div>
102
+ <button mat-menu-item type="button" (click)="setLlmMode('catalog')">
103
+ <mat-icon>{{ llmMode() === 'catalog' ? 'check' : 'widgets' }}</mat-icon>
104
+ <span>Com cat\xE1logo (comandos do editor)</span>
105
+ </button>
106
+ <button mat-menu-item type="button" (click)="setLlmMode('raw-svg')">
107
+ <mat-icon>{{ llmMode() === 'raw-svg' ? 'check' : 'code' }}</mat-icon>
108
+ <span>SVG livre (a IA desenha o SVG)</span>
109
+ </button>
110
+ @if (modelOptions().length > 0) {
111
+ <div class="svge-nlu-menu-title">Modelo</div>
112
+ @for (m of modelOptions(); track m) {
113
+ <button mat-menu-item type="button" (click)="selectModel(m)">
114
+ <mat-icon>{{ effectiveModel() === m ? 'check' : 'memory' }}</mat-icon>
115
+ <span>{{ m }}</span>
116
+ </button>
117
+ }
118
+ }
119
+ </mat-menu>
120
+ <button
121
+ mat-icon-button
122
+ type="button"
123
+ class="svge-nlu-ask-ai"
124
+ [matTooltip]="
125
+ llmMode() === 'raw-svg'
126
+ ? 'Pedir \xE0 IA (gerar SVG livre)'
127
+ : 'Pedir \xE0 IA (ignora o reconhecimento por regras)'
128
+ "
129
+ aria-label="Pedir \xE0 IA"
130
+ [disabled]="text().trim().length === 0 || llmThinking()"
131
+ (click)="askAi()"
132
+ >
133
+ <mat-icon>auto_awesome</mat-icon>
134
+ </button>
135
+ }
136
+ <button
137
+ mat-icon-button
138
+ type="button"
139
+ class="svge-nlu-run"
140
+ matTooltip="Run (Enter)"
141
+ [disabled]="text().trim().length === 0"
142
+ (click)="runNow()"
143
+ >
144
+ <mat-icon>send</mat-icon>
145
+ </button>
146
+ </span>
147
+ </mat-form-field>
148
+
149
+ @if (topCandidate(); as top) {
150
+ <div class="svge-nlu-hint" id="svge-nlu-hint" role="status">
151
+ <span class="svge-nlu-hint-label">{{ describeIntent(top) }}</span>
152
+ <span class="svge-nlu-hint-confidence" [class.low]="top.confidence < 0.6">
153
+ {{ confidencePercent(top) }}%
154
+ </span>
155
+ </div>
156
+ <mat-progress-bar
157
+ mode="determinate"
158
+ [value]="top.confidence * 100"
159
+ [color]="top.confidence >= 0.7 ? 'primary' : top.confidence >= 0.4 ? 'accent' : 'warn'"
160
+ class="svge-nlu-confidence-bar"
161
+ />
162
+ }
163
+
164
+ @if (lastResult(); as result) {
165
+ <div class="svge-nlu-status" role="status" aria-live="polite">
166
+ @if (result.executed) {
167
+ <mat-icon class="ok" aria-hidden="true">check_circle</mat-icon>
168
+ <span
169
+ >Executado: <strong>{{ describeIntent(result.candidate!) }}</strong></span
170
+ >
171
+ } @else {
172
+ <mat-icon class="warn" aria-hidden="true">info</mat-icon>
173
+ <span class="svge-nlu-status-text">{{ describeRejection(result) }}</span>
174
+ @if (canForceExecute(result)) {
175
+ <button
176
+ mat-stroked-button
177
+ type="button"
178
+ color="primary"
179
+ class="svge-nlu-confirm-btn"
180
+ (click)="forceExecute(result)"
181
+ >
182
+ <mat-icon>play_arrow</mat-icon>
183
+ <span>Confirmar</span>
184
+ </button>
185
+ }
186
+ }
187
+ </div>
188
+ }
189
+
190
+ @if (llmThinking()) {
191
+ <div class="svge-nlu-status" role="status" aria-live="polite">
192
+ <mat-icon class="svge-nlu-llm-icon" aria-hidden="true">auto_awesome</mat-icon>
193
+ <span class="svge-nlu-status-text"
194
+ >IA interpretando o pedido\u2026 pode levar alguns segundos.</span
195
+ >
196
+ </div>
197
+ <mat-progress-bar mode="indeterminate" class="svge-nlu-confidence-bar" />
198
+ }
199
+
200
+ @if (llmError(); as msg) {
201
+ <p class="svge-nlu-voice-error" role="alert">{{ msg }}</p>
202
+ }
203
+
204
+ @if (alternatives().length > 0) {
205
+ <details class="svge-nlu-alts">
206
+ <summary>{{ alternatives().length }} alternativa(s)</summary>
207
+ <ul role="listbox">
208
+ @for (alt of alternatives(); track alt.intent.id) {
209
+ <li
210
+ role="option"
211
+ [attr.aria-selected]="false"
212
+ class="svge-nlu-alt"
213
+ (click)="execCandidate(alt)"
214
+ (keydown.enter)="execCandidate(alt)"
215
+ tabindex="0"
216
+ >
217
+ <span class="svge-nlu-alt-label">{{ describeIntent(alt) }}</span>
218
+ <span class="svge-nlu-alt-confidence">{{ confidencePercent(alt) }}%</span>
219
+ </li>
220
+ }
221
+ </ul>
222
+ </details>
223
+ }
224
+
225
+ @if (voiceErrorMessage(); as msg) {
226
+ <p class="svge-nlu-voice-error" role="alert">{{ msg }}</p>
227
+ }
228
+ </div>
229
+ `,isInline:!0,styles:[`:host{display:block}.svge-nlu-root{display:flex;flex-direction:column;gap:4px}.svge-nlu-field{width:100%}.svge-nlu-actions{display:inline-flex;align-items:center;gap:0}.svge-nlu-prefix{color:var(--mat-sys-primary, #1976d2);margin-right:6px}.svge-nlu-mic.recording{color:var(--mat-sys-error, #d32f2f);animation:svge-nlu-pulse 1.2s ease-in-out infinite}@keyframes svge-nlu-pulse{0%,to{opacity:1}50%{opacity:.4}}.svge-nlu-hint{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 8px;font-size:12px;color:var(--mat-sys-on-surface-variant, #666)}.svge-nlu-hint-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svge-nlu-hint-confidence{flex:0 0 auto;font-variant-numeric:tabular-nums;font-weight:500;color:var(--mat-sys-primary, #1976d2)}.svge-nlu-hint-confidence.low{color:var(--mat-sys-tertiary, #b26500)}.svge-nlu-confidence-bar{height:3px;border-radius:2px}.svge-nlu-status{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-top:6px;border-radius:6px;background:var(--mat-sys-surface-container, rgba(0, 0, 0, .04));font-size:13px}.svge-nlu-status-text{flex:1 1 auto;min-width:0}.svge-nlu-status .ok{color:#2e7d32}.svge-nlu-status .warn{color:var(--mat-sys-tertiary, #b26500)}.svge-nlu-confirm-btn{flex:0 0 auto}.svge-nlu-alts{margin-top:6px;padding:6px 12px;background:var(--mat-sys-surface-container, rgba(0, 0, 0, .04));border-radius:6px;font-size:13px}.svge-nlu-alts summary{cursor:pointer;color:var(--mat-sys-on-surface-variant, #666)}.svge-nlu-alts ul{list-style:none;margin:6px 0 0;padding:0}.svge-nlu-alt{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;cursor:pointer;border-radius:4px}.svge-nlu-alt:hover,.svge-nlu-alt:focus-visible{background:var(--mat-sys-surface-variant, rgba(0, 0, 0, .06));outline:none}.svge-nlu-alt-confidence{font-variant-numeric:tabular-nums;color:var(--mat-sys-primary, #1976d2);font-size:12px}.svge-nlu-voice-error{margin:6px 0 0;padding:6px 12px;border-radius:6px;background:var(--mat-sys-error-container, #fde7e9);color:var(--mat-sys-on-error-container, #5a1014);font-size:12px}.svge-nlu-menu-title{padding:8px 16px 2px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--mat-sys-on-surface-variant, #666);cursor:default}
230
+ `],dependencies:[{kind:"ngmodule",type:E},{kind:"ngmodule",type:N},{kind:"component",type:D.MatButton,selector:" button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ",inputs:["matButton"],exportAs:["matButton","matAnchor"]},{kind:"component",type:D.MatIconButton,selector:"button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]",exportAs:["matButton","matAnchor"]},{kind:"component",type:R,selector:"mat-form-field",inputs:["hideRequiredMarker","color","floatLabel","appearance","subscriptSizing","hintLabel"],exportAs:["matFormField"]},{kind:"directive",type:A,selector:"mat-label"},{kind:"directive",type:P,selector:"[matPrefix], [matIconPrefix], [matTextPrefix]",inputs:["matTextPrefix"]},{kind:"directive",type:_,selector:"[matSuffix], [matIconSuffix], [matTextSuffix]",inputs:["matTextSuffix"]},{kind:"component",type:C,selector:"mat-icon",inputs:["color","inline","svgIcon","fontSet","fontIcon"],exportAs:["matIcon"]},{kind:"directive",type:F,selector:"input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",inputs:["disabled","id","placeholder","name","required","type","errorStateMatcher","aria-describedby","value","readonly","disabledInteractive"],exportAs:["matInput"]},{kind:"ngmodule",type:V},{kind:"component",type:y.MatMenu,selector:"mat-menu",inputs:["backdropClass","aria-label","aria-labelledby","aria-describedby","xPosition","yPosition","overlapTrigger","hasBackdrop","class","classList"],outputs:["closed","close"],exportAs:["matMenu"]},{kind:"component",type:y.MatMenuItem,selector:"[mat-menu-item]",inputs:["role","disabled","disableRipple"],exportAs:["matMenuItem"]},{kind:"directive",type:y.MatMenuTrigger,selector:"[mat-menu-trigger-for], [matMenuTriggerFor]",inputs:["mat-menu-trigger-for","matMenuTriggerFor","matMenuTriggerData","matMenuTriggerRestoreFocus"],outputs:["menuOpened","onMenuOpen","menuClosed","onMenuClose"],exportAs:["matMenuTrigger"]},{kind:"ngmodule",type:j},{kind:"component",type:Y.MatProgressBar,selector:"mat-progress-bar",inputs:["color","value","bufferValue","mode"],outputs:["animationEnd"],exportAs:["matProgressBar"]},{kind:"directive",type:O,selector:"[matTooltip]",inputs:["matTooltipPosition","matTooltipPositionAtOrigin","matTooltipDisabled","matTooltipShowDelay","matTooltipHideDelay","matTooltipTouchGestures","matTooltip","matTooltipClass"],exportAs:["matTooltip"]}],changeDetection:n.ChangeDetectionStrategy.OnPush})}n.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"21.2.13",ngImport:n,type:h,decorators:[{type:X,args:[{selector:"svge-nlu-input",standalone:!0,imports:[E,N,R,A,P,_,C,F,V,j,O],template:`
231
+ <div class="svge-nlu-root">
232
+ <mat-form-field appearance="outline" class="svge-nlu-field">
233
+ <mat-label>{{ label() }}</mat-label>
234
+ <mat-icon matPrefix class="svge-nlu-prefix" aria-hidden="true">smart_toy</mat-icon>
235
+ <input
236
+ #textInput
237
+ matInput
238
+ type="text"
239
+ (input)="onInput($any($event.target).value)"
240
+ (keydown.enter)="runNow()"
241
+ [attr.aria-label]="label()"
242
+ [attr.aria-describedby]="topCandidate() ? 'svge-nlu-hint' : null"
243
+ [placeholder]="placeholder()"
244
+ />
245
+ <!--
246
+ Grupo de a\xE7\xF5es \xDANICO (matSuffix) \u2014 idioma + motor de voz +
247
+ microfone + run ficam LADO A LADO \xE0 direita do input. Antes
248
+ cada bot\xE3o era um matSuffix separado dentro de um bloco
249
+ condicional, e o form-field MDC posicionava os condicionais
250
+ errado (idioma/motor ca\xEDam \xE0 esquerda/embaixo). Um matSuffix
251
+ SEMPRE presente (flex) resolve: o Material s\xF3 ancora o
252
+ cont\xEAiner; os condicionais internos viram DOM comum. Os
253
+ mat-menu ficam junto de seus bot\xF5es (mesmo bloco) para
254
+ preservar o escopo do template ref.
255
+ -->
256
+ <span matSuffix class="svge-nlu-actions">
257
+ @if (voice.isSupported()) {
258
+ <button
259
+ mat-icon-button
260
+ type="button"
261
+ class="svge-nlu-lang"
262
+ [matMenuTriggerFor]="langMenu"
263
+ [matTooltip]="'Idioma da voz: ' + languageShort(selectedLanguage())"
264
+ aria-label="Selecionar idioma da voz"
265
+ >
266
+ <mat-icon>translate</mat-icon>
267
+ </button>
268
+ <mat-menu #langMenu="matMenu">
269
+ @for (l of languages; track l.code) {
270
+ <button mat-menu-item type="button" (click)="setLanguage(l.code)">
271
+ <mat-icon>{{ selectedLanguage() === l.code ? 'check' : 'language' }}</mat-icon>
272
+ <span>{{ l.label }}</span>
273
+ </button>
274
+ }
275
+ </mat-menu>
276
+ }
277
+ @if (voice.availableEngines().length > 1) {
278
+ <button
279
+ mat-icon-button
280
+ type="button"
281
+ class="svge-nlu-engine"
282
+ [matMenuTriggerFor]="engineMenu"
283
+ [matTooltip]="'Motor de voz: ' + engineLabel(voice.engine())"
284
+ aria-label="Selecionar motor de voz"
285
+ >
286
+ <mat-icon>{{ engineIcon(voice.engine()) }}</mat-icon>
287
+ </button>
288
+ <mat-menu #engineMenu="matMenu">
289
+ @for (e of voice.availableEngines(); track e) {
290
+ <button mat-menu-item type="button" (click)="voice.setEngine(e)">
291
+ <mat-icon>{{ voice.engine() === e ? 'check' : engineIcon(e) }}</mat-icon>
292
+ <span>{{ engineLabel(e) }}</span>
293
+ </button>
294
+ }
295
+ </mat-menu>
296
+ }
297
+ @if (voice.isSupported()) {
298
+ <button
299
+ mat-icon-button
300
+ type="button"
301
+ class="svge-nlu-mic"
302
+ [class.recording]="voice.listening()"
303
+ [disabled]="voice.modelLoading()"
304
+ [attr.aria-pressed]="voice.listening()"
305
+ [matTooltip]="micTooltip()"
306
+ (click)="toggleVoice()"
307
+ >
308
+ <mat-icon>{{
309
+ voice.modelLoading() ? 'hourglass_empty' : voice.listening() ? 'mic_off' : 'mic'
310
+ }}</mat-icon>
311
+ </button>
312
+ }
313
+ @if (llm.isAvailable) {
314
+ <button
315
+ mat-icon-button
316
+ type="button"
317
+ class="svge-nlu-ai-config"
318
+ [matMenuTriggerFor]="aiMenu"
319
+ [matTooltip]="
320
+ 'IA: ' +
321
+ (llmMode() === 'raw-svg' ? 'SVG livre' : 'com cat\xE1logo') +
322
+ ' \xB7 modelo ' +
323
+ (effectiveModel() ?? 'padr\xE3o')
324
+ "
325
+ aria-label="Configurar IA (modo e modelo)"
326
+ >
327
+ <mat-icon>tune</mat-icon>
328
+ </button>
329
+ <mat-menu #aiMenu="matMenu" class="svge-nlu-ai-menu">
330
+ <div class="svge-nlu-menu-title">Modo de gera\xE7\xE3o</div>
331
+ <button mat-menu-item type="button" (click)="setLlmMode('catalog')">
332
+ <mat-icon>{{ llmMode() === 'catalog' ? 'check' : 'widgets' }}</mat-icon>
333
+ <span>Com cat\xE1logo (comandos do editor)</span>
334
+ </button>
335
+ <button mat-menu-item type="button" (click)="setLlmMode('raw-svg')">
336
+ <mat-icon>{{ llmMode() === 'raw-svg' ? 'check' : 'code' }}</mat-icon>
337
+ <span>SVG livre (a IA desenha o SVG)</span>
338
+ </button>
339
+ @if (modelOptions().length > 0) {
340
+ <div class="svge-nlu-menu-title">Modelo</div>
341
+ @for (m of modelOptions(); track m) {
342
+ <button mat-menu-item type="button" (click)="selectModel(m)">
343
+ <mat-icon>{{ effectiveModel() === m ? 'check' : 'memory' }}</mat-icon>
344
+ <span>{{ m }}</span>
345
+ </button>
346
+ }
347
+ }
348
+ </mat-menu>
349
+ <button
350
+ mat-icon-button
351
+ type="button"
352
+ class="svge-nlu-ask-ai"
353
+ [matTooltip]="
354
+ llmMode() === 'raw-svg'
355
+ ? 'Pedir \xE0 IA (gerar SVG livre)'
356
+ : 'Pedir \xE0 IA (ignora o reconhecimento por regras)'
357
+ "
358
+ aria-label="Pedir \xE0 IA"
359
+ [disabled]="text().trim().length === 0 || llmThinking()"
360
+ (click)="askAi()"
361
+ >
362
+ <mat-icon>auto_awesome</mat-icon>
363
+ </button>
364
+ }
365
+ <button
366
+ mat-icon-button
367
+ type="button"
368
+ class="svge-nlu-run"
369
+ matTooltip="Run (Enter)"
370
+ [disabled]="text().trim().length === 0"
371
+ (click)="runNow()"
372
+ >
373
+ <mat-icon>send</mat-icon>
374
+ </button>
375
+ </span>
376
+ </mat-form-field>
377
+
378
+ @if (topCandidate(); as top) {
379
+ <div class="svge-nlu-hint" id="svge-nlu-hint" role="status">
380
+ <span class="svge-nlu-hint-label">{{ describeIntent(top) }}</span>
381
+ <span class="svge-nlu-hint-confidence" [class.low]="top.confidence < 0.6">
382
+ {{ confidencePercent(top) }}%
383
+ </span>
384
+ </div>
385
+ <mat-progress-bar
386
+ mode="determinate"
387
+ [value]="top.confidence * 100"
388
+ [color]="top.confidence >= 0.7 ? 'primary' : top.confidence >= 0.4 ? 'accent' : 'warn'"
389
+ class="svge-nlu-confidence-bar"
390
+ />
391
+ }
392
+
393
+ @if (lastResult(); as result) {
394
+ <div class="svge-nlu-status" role="status" aria-live="polite">
395
+ @if (result.executed) {
396
+ <mat-icon class="ok" aria-hidden="true">check_circle</mat-icon>
397
+ <span
398
+ >Executado: <strong>{{ describeIntent(result.candidate!) }}</strong></span
399
+ >
400
+ } @else {
401
+ <mat-icon class="warn" aria-hidden="true">info</mat-icon>
402
+ <span class="svge-nlu-status-text">{{ describeRejection(result) }}</span>
403
+ @if (canForceExecute(result)) {
404
+ <button
405
+ mat-stroked-button
406
+ type="button"
407
+ color="primary"
408
+ class="svge-nlu-confirm-btn"
409
+ (click)="forceExecute(result)"
410
+ >
411
+ <mat-icon>play_arrow</mat-icon>
412
+ <span>Confirmar</span>
413
+ </button>
414
+ }
415
+ }
416
+ </div>
417
+ }
418
+
419
+ @if (llmThinking()) {
420
+ <div class="svge-nlu-status" role="status" aria-live="polite">
421
+ <mat-icon class="svge-nlu-llm-icon" aria-hidden="true">auto_awesome</mat-icon>
422
+ <span class="svge-nlu-status-text"
423
+ >IA interpretando o pedido\u2026 pode levar alguns segundos.</span
424
+ >
425
+ </div>
426
+ <mat-progress-bar mode="indeterminate" class="svge-nlu-confidence-bar" />
427
+ }
428
+
429
+ @if (llmError(); as msg) {
430
+ <p class="svge-nlu-voice-error" role="alert">{{ msg }}</p>
431
+ }
432
+
433
+ @if (alternatives().length > 0) {
434
+ <details class="svge-nlu-alts">
435
+ <summary>{{ alternatives().length }} alternativa(s)</summary>
436
+ <ul role="listbox">
437
+ @for (alt of alternatives(); track alt.intent.id) {
438
+ <li
439
+ role="option"
440
+ [attr.aria-selected]="false"
441
+ class="svge-nlu-alt"
442
+ (click)="execCandidate(alt)"
443
+ (keydown.enter)="execCandidate(alt)"
444
+ tabindex="0"
445
+ >
446
+ <span class="svge-nlu-alt-label">{{ describeIntent(alt) }}</span>
447
+ <span class="svge-nlu-alt-confidence">{{ confidencePercent(alt) }}%</span>
448
+ </li>
449
+ }
450
+ </ul>
451
+ </details>
452
+ }
453
+
454
+ @if (voiceErrorMessage(); as msg) {
455
+ <p class="svge-nlu-voice-error" role="alert">{{ msg }}</p>
456
+ }
457
+ </div>
458
+ `,changeDetection:Q.OnPush,styles:[`:host{display:block}.svge-nlu-root{display:flex;flex-direction:column;gap:4px}.svge-nlu-field{width:100%}.svge-nlu-actions{display:inline-flex;align-items:center;gap:0}.svge-nlu-prefix{color:var(--mat-sys-primary, #1976d2);margin-right:6px}.svge-nlu-mic.recording{color:var(--mat-sys-error, #d32f2f);animation:svge-nlu-pulse 1.2s ease-in-out infinite}@keyframes svge-nlu-pulse{0%,to{opacity:1}50%{opacity:.4}}.svge-nlu-hint{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:4px 8px;font-size:12px;color:var(--mat-sys-on-surface-variant, #666)}.svge-nlu-hint-label{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.svge-nlu-hint-confidence{flex:0 0 auto;font-variant-numeric:tabular-nums;font-weight:500;color:var(--mat-sys-primary, #1976d2)}.svge-nlu-hint-confidence.low{color:var(--mat-sys-tertiary, #b26500)}.svge-nlu-confidence-bar{height:3px;border-radius:2px}.svge-nlu-status{display:flex;align-items:center;gap:8px;padding:8px 12px;margin-top:6px;border-radius:6px;background:var(--mat-sys-surface-container, rgba(0, 0, 0, .04));font-size:13px}.svge-nlu-status-text{flex:1 1 auto;min-width:0}.svge-nlu-status .ok{color:#2e7d32}.svge-nlu-status .warn{color:var(--mat-sys-tertiary, #b26500)}.svge-nlu-confirm-btn{flex:0 0 auto}.svge-nlu-alts{margin-top:6px;padding:6px 12px;background:var(--mat-sys-surface-container, rgba(0, 0, 0, .04));border-radius:6px;font-size:13px}.svge-nlu-alts summary{cursor:pointer;color:var(--mat-sys-on-surface-variant, #666)}.svge-nlu-alts ul{list-style:none;margin:6px 0 0;padding:0}.svge-nlu-alt{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;cursor:pointer;border-radius:4px}.svge-nlu-alt:hover,.svge-nlu-alt:focus-visible{background:var(--mat-sys-surface-variant, rgba(0, 0, 0, .06));outline:none}.svge-nlu-alt-confidence{font-variant-numeric:tabular-nums;color:var(--mat-sys-primary, #1976d2);font-size:12px}.svge-nlu-voice-error{margin:6px 0 0;padding:6px 12px;border-radius:6px;background:var(--mat-sys-error-container, #fde7e9);color:var(--mat-sys-on-error-container, #5a1014);font-size:12px}.svge-nlu-menu-title{padding:8px 16px 2px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--mat-sys-on-surface-variant, #666);cursor:default}
459
+ `]}]}],ctorParameters:()=>[],propDecorators:{label:[{type:n.Input,args:[{isSignal:!0,alias:"label",required:!1}]}],placeholder:[{type:n.Input,args:[{isSignal:!0,alias:"placeholder",required:!1}]}],voiceLang:[{type:n.Input,args:[{isSignal:!0,alias:"voiceLang",required:!1}]}],autoDetectLanguage:[{type:n.Input,args:[{isSignal:!0,alias:"autoDetectLanguage",required:!1}]}],parseDebounceMs:[{type:n.Input,args:[{isSignal:!0,alias:"parseDebounceMs",required:!1}]}],autoExecuteThreshold:[{type:n.Input,args:[{isSignal:!0,alias:"autoExecuteThreshold",required:!1}]}],confirmGate:[{type:n.Input,args:[{isSignal:!0,alias:"confirmGate",required:!1}]}],enableLlmFallback:[{type:n.Input,args:[{isSignal:!0,alias:"enableLlmFallback",required:!1}]}],llmModel:[{type:n.Input,args:[{isSignal:!0,alias:"llmModel",required:!1}]}],executed:[{type:n.Output,args:["executed"]}],textInputRef:[{type:n.ViewChild,args:["textInput",{isSignal:!0}]}]}});export{h as SvgeNluInput,v as VoiceEngineService,m as VoiceRecognitionService};
@@ -0,0 +1 @@
1
+ import*as f from"@angular/core";import{InjectionToken as z,inject as F,signal as w,Injectable as G}from"@angular/core";import{VOICE_WHISPER_PROVIDER as j}from"@mosaicoo/svg-engine/ai/nlu";const b={modelBasePath:"/assets/ml/whisper",modelId:"whisper-small",wasmBasePath:"/assets/ml/ort/",dtype:"q8",numThreads:1,graphOptimizationLevel:"disabled",defaultLanguage:"pt",maxRecordMs:15e3,silenceMs:1e3,silenceThreshold:.015,noSpeechTimeoutMs:6e3},S=new z("WHISPER_VOICE_CONFIG",{providedIn:"root",factory:()=>b});function V(a={}){return{provide:S,useValue:{...b,...a}}}function H(){return F(S)}const R={pt:"portuguese",es:"spanish",en:"english"};function U(a,e){const i=a.toLowerCase(),n=i.split("-")[0]??i;return R[i]??R[n]??R[e]??"portuguese"}class p{config=H();isSupported=w(q()).asReadonly();_listening=w(!1,...ngDevMode?[{debugName:"_listening"}]:[]);listening=this._listening.asReadonly();_modelLoading=w(!1,...ngDevMode?[{debugName:"_modelLoading"}]:[]);modelLoading=this._modelLoading.asReadonly();_lastError=w(null,...ngDevMode?[{debugName:"_lastError"}]:[]);lastError=this._lastError.asReadonly();transcriber=null;activeRecorder=null;activeStream=null;listen(e,i={}){const n=U(e??this.config.defaultLanguage,this.config.defaultLanguage),c=i.timeoutMs??this.config.maxRecordMs;return this._lastError.set(null),new Promise((h,d)=>{if(!this.isSupported()){this._lastError.set("not-supported"),d(new Error("WhisperVoiceService: getUserMedia/WebAssembly not available"));return}let o=!1,u=null,m=null,g=null;const I=[],x=()=>{if(u!==null&&clearTimeout(u),m!==null&&clearInterval(m),g!==null&&g.close(),m=null,g=null,this.activeStream!==null)for(const s of this.activeStream.getTracks())s.stop();this.activeRecorder=null,this.activeStream=null,this._listening.set(!1)},y=(s,t)=>{o||(o=!0,this._lastError.set(s),x(),d(t instanceof Error?t:new Error(String(t))))};navigator.mediaDevices.getUserMedia({audio:!0}).then(s=>{if(o){for(const r of s.getTracks())r.stop();return}this.activeStream=s;const t=new MediaRecorder(s);if(this.activeRecorder=t,t.ondataavailable=r=>{r.data.size>0&&I.push(r.data)},t.onerror=()=>y("audio-capture",new Error("MediaRecorder error")),t.onstop=()=>{if(o)return;const r=new Blob(I,{type:t.mimeType||"audio/webm"});this.transcribe(r,n).then(l=>{o||(o=!0,x(),h(l))}).catch(l=>{console.error("[WhisperVoice] falha ao transcrever:",l);const E=this._lastError();y(E??"transcribe-failed",l)})},t.start(),this._listening.set(!0),c>0&&(u=setTimeout(()=>{t.state!=="inactive"&&t.stop()},c)),this.config.silenceMs>0){const r=O();if(r!==null){const l=new r;g=l,l.resume();const E=l.createMediaStreamSource(s),v=l.createAnalyser();v.fftSize=512,E.connect(v);const _=new Uint8Array(v.fftSize),M=100,W=Math.max(1,Math.round(this.config.silenceMs/M)),A=this.config.noSpeechTimeoutMs>0?Math.max(1,Math.round(this.config.noSpeechTimeoutMs/M)):0,N=this.config.silenceThreshold;let C=!1,T=0,L=0;const k=()=>{t.state!=="inactive"&&t.stop()};m=setInterval(()=>{L++,v.getByteTimeDomainData(_);let D=0;for(const B of _){const P=(B-128)/128;D+=P*P}Math.sqrt(D/_.length)>=N?(C=!0,T=0):C?(T++,T>=W&&k()):A>0&&L>=A&&k()},M)}}}).catch(s=>{const r=(s instanceof DOMException?s.name:"")==="NotAllowedError"?"not-allowed":"audio-capture";y(r,s)})})}stop(){const e=this.activeRecorder;e!==null&&e.state!=="inactive"&&e.stop()}async transcribe(e,i){const n=await $(e),c=n.length/16e3,d=await(await this.ensurePipeline())(n,{language:i,task:"transcribe",temperature:0,no_repeat_ngram_size:3,chunk_length_s:30,return_timestamps:!1}),o=(Array.isArray(d)?d[0]?.text??"":d.text??"").trim();return console.info(`[WhisperVoice] transcri\xE7\xE3o (${c.toFixed(1)}s, ${i}): ${JSON.stringify(o)}`),o}async ensurePipeline(){if(this.transcriber!==null)return this.transcriber;this._modelLoading.set(!0);try{const{env:e,pipeline:i}=await import("@huggingface/transformers");e.allowRemoteModels=!1,e.allowLocalModels=!0,e.localModelPath=this.config.modelBasePath;const n=e.backends?.onnx?.wasm;n!==void 0&&(n.wasmPaths=this.config.wasmBasePath,n.numThreads=this.config.numThreads);const c=await i("automatic-speech-recognition",this.config.modelId,{dtype:this.config.dtype,device:"wasm",session_options:{graphOptimizationLevel:this.config.graphOptimizationLevel}});return this.transcriber=c,c}catch(e){throw console.error("[WhisperVoice] falha ao carregar o modelo (pipeline):",e),this._lastError.set("load-failed"),e instanceof Error?e:new Error(String(e))}finally{this._modelLoading.set(!1)}}static \u0275fac=f.\u0275\u0275ngDeclareFactory({minVersion:"12.0.0",version:"21.2.13",ngImport:f,type:p,deps:[],target:f.\u0275\u0275FactoryTarget.Injectable});static \u0275prov=f.\u0275\u0275ngDeclareInjectable({minVersion:"12.0.0",version:"21.2.13",ngImport:f,type:p,providedIn:"root"})}f.\u0275\u0275ngDeclareClassMetadata({minVersion:"12.0.0",version:"21.2.13",ngImport:f,type:p,decorators:[{type:G,args:[{providedIn:"root"}]}]});function q(){return typeof navigator>"u"||typeof WebAssembly>"u"?!1:typeof navigator.mediaDevices?.getUserMedia=="function"}async function $(a){const i=await a.arrayBuffer(),n=O();if(n===null)throw new Error("AudioContext unavailable");const c=new n;let h;try{h=await c.decodeAudioData(i)}finally{c.close()}const d=Math.max(1,Math.ceil(h.duration*16e3)),o=new OfflineAudioContext(1,d,16e3),u=o.createBufferSource();return u.buffer=h,u.connect(o.destination),u.start(0),(await o.startRendering()).getChannelData(0).slice()}function O(){if(typeof window>"u")return null;const a=window;return a.AudioContext??a.webkitAudioContext??null}function J(a={}){return[V(a),{provide:j,useExisting:p}]}export{b as DEFAULT_WHISPER_VOICE_CONFIG,S as WHISPER_VOICE_CONFIG,p as WhisperVoiceService,V as provideWhisperVoice,J as provideWhisperVoiceEngine};