@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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +249 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu-ui.mjs +459 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu-voice-wasm.mjs +1 -0
- package/fesm2022/mosaicoo-svg-engine-ai-nlu.mjs +11 -0
- package/fesm2022/mosaicoo-svg-engine-core.mjs +3 -0
- package/fesm2022/mosaicoo-svg-engine-edit.mjs +2292 -0
- package/fesm2022/mosaicoo-svg-engine-io.mjs +47 -0
- package/fesm2022/mosaicoo-svg-engine-optimize.mjs +1 -0
- package/fesm2022/mosaicoo-svg-engine-render.mjs +301 -0
- package/fesm2022/mosaicoo-svg-engine-ui.mjs +14236 -0
- package/fesm2022/mosaicoo-svg-engine.mjs +1 -0
- package/package.json +105 -0
- package/types/mosaicoo-svg-engine-ai-nlu-ui.d.ts +416 -0
- package/types/mosaicoo-svg-engine-ai-nlu-voice-wasm.d.ts +175 -0
- package/types/mosaicoo-svg-engine-ai-nlu.d.ts +1834 -0
- package/types/mosaicoo-svg-engine-core.d.ts +5139 -0
- package/types/mosaicoo-svg-engine-edit.d.ts +11922 -0
- package/types/mosaicoo-svg-engine-io.d.ts +476 -0
- package/types/mosaicoo-svg-engine-optimize.d.ts +183 -0
- package/types/mosaicoo-svg-engine-render.d.ts +628 -0
- package/types/mosaicoo-svg-engine-ui.d.ts +6861 -0
- package/types/mosaicoo-svg-engine.d.ts +30 -0
|
@@ -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};
|