@sola-air-ui/core 1.0.3 → 1.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.
@@ -40,6 +40,157 @@ var SolaCore = (() => {
40
40
  pushContext: () => pushContext,
41
41
  signalMesh: () => signalMesh
42
42
  });
43
+
44
+ // src/sentinel.js
45
+ var FIELD_BUFFER_MAX_EVENTS = 50;
46
+ var FIELD_BUFFER_WINDOW_MS = 6e4;
47
+ var FIELD_TEXT_PREVIEW_MAX_CHARS = 200;
48
+ function now() {
49
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
50
+ }
51
+ var SolaSentinel = class {
52
+ constructor(name = "default", options = {}) {
53
+ this.name = name;
54
+ this.thresholdMs = options.thresholdMs || 600;
55
+ this.maxRageClicks = options.maxRageClicks || 3;
56
+ this.clickHistory = [];
57
+ this.subscribers = /* @__PURE__ */ new Set();
58
+ this.frictionEvents = [];
59
+ this.flowIndex = 99.8;
60
+ this.fieldHistory = [];
61
+ this.lastActivityAt = 0;
62
+ this.lastSuggestedAt = -Infinity;
63
+ this.idleThresholdMs = options.idleThresholdMs ?? 1500;
64
+ this.minSuggestIntervalMs = options.minSuggestIntervalMs ?? 8e3;
65
+ this.minEventsForSuggestion = options.minEventsForSuggestion ?? 2;
66
+ }
67
+ recordClick(actionId, target = "button") {
68
+ const ts = now();
69
+ this.clickHistory.push({ actionId, target, timestamp: ts });
70
+ this.clickHistory = this.clickHistory.filter((c) => ts - c.timestamp < 2e3);
71
+ const recent = this.clickHistory.filter((c) => c.actionId === actionId && ts - c.timestamp < this.thresholdMs);
72
+ if (recent.length >= this.maxRageClicks) {
73
+ this.triggerFrictionAlert({
74
+ type: "RAGE_CLICK",
75
+ actionId,
76
+ target,
77
+ count: recent.length,
78
+ timestamp: ts,
79
+ severity: "HIGH",
80
+ message: `Rage-click burst: ${recent.length} taps in ${Math.round(ts - recent[0].timestamp)}ms`
81
+ });
82
+ }
83
+ }
84
+ recordSignalDrop(topic, error) {
85
+ this.triggerFrictionAlert({
86
+ type: "SIGNAL_TIMEOUT",
87
+ topic,
88
+ error: error?.message || String(error),
89
+ timestamp: now(),
90
+ severity: "CRITICAL",
91
+ message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
92
+ });
93
+ }
94
+ triggerFrictionAlert(event) {
95
+ this.frictionEvents.unshift(event);
96
+ if (this.frictionEvents.length > 50) this.frictionEvents.pop();
97
+ this._recomputeFlowIndex(event.timestamp);
98
+ this.subscribers.forEach((cb) => {
99
+ try {
100
+ cb(event, this);
101
+ } catch (e) {
102
+ console.error(e);
103
+ }
104
+ });
105
+ }
106
+ onFriction(cb) {
107
+ this.subscribers.add(cb);
108
+ return () => this.subscribers.delete(cb);
109
+ }
110
+ // ─── Flow index ───
111
+ // A real computed score, not a fixed decrement: severity- and recency-weighted
112
+ // friction events, the share of field visits that were backtracks, plus how
113
+ // erratic the pacing between field events is (a proxy for hesitation).
114
+ _recomputeFlowIndex(ts = now()) {
115
+ let score = 99.8;
116
+ const recentFriction = this.frictionEvents.filter((e) => ts - e.timestamp < 12e4);
117
+ score -= recentFriction.reduce((sum, e) => {
118
+ const severityWeight = e.severity === "CRITICAL" ? 6 : e.severity === "HIGH" ? 3.8 : 2;
119
+ const recencyWeight = Math.max(0.3, 1 - (ts - e.timestamp) / 12e4);
120
+ return sum + severityWeight * recencyWeight;
121
+ }, 0);
122
+ const focusEvents = this.fieldHistory.filter((e) => e.type === "focus");
123
+ if (focusEvents.length > 0) {
124
+ const revisitRatio = focusEvents.filter((e) => e.revisit).length / focusEvents.length;
125
+ score -= revisitRatio * 15;
126
+ }
127
+ if (this.fieldHistory.length >= 3) {
128
+ const gaps = [];
129
+ for (let i = 1; i < this.fieldHistory.length; i++) {
130
+ gaps.push(this.fieldHistory[i].timestamp - this.fieldHistory[i - 1].timestamp);
131
+ }
132
+ const mean = gaps.reduce((a, b) => a + b, 0) / gaps.length;
133
+ const variance = gaps.reduce((sum, g) => sum + (g - mean) ** 2, 0) / gaps.length;
134
+ score -= Math.min(10, Math.sqrt(variance) / 500);
135
+ }
136
+ this.flowIndex = Math.max(0, Math.min(99.8, Number(score.toFixed(1))));
137
+ return this.flowIndex;
138
+ }
139
+ // ─── Ambient field observation ───
140
+ _pushFieldEvent(event) {
141
+ this.fieldHistory.push(event);
142
+ this.fieldHistory = this.fieldHistory.filter((e) => event.timestamp - e.timestamp < FIELD_BUFFER_WINDOW_MS).slice(-FIELD_BUFFER_MAX_EVENTS);
143
+ this.lastActivityAt = event.timestamp;
144
+ this._recomputeFlowIndex(event.timestamp);
145
+ }
146
+ recordFieldFocus(fieldId, ts = now()) {
147
+ const revisit = this.fieldHistory.some((e) => e.type === "blur" && e.fieldId === fieldId);
148
+ this._pushFieldEvent({ type: "focus", fieldId, revisit, timestamp: ts });
149
+ return revisit;
150
+ }
151
+ recordFieldBlur(fieldId, value, ts = now()) {
152
+ const text = String(value ?? "");
153
+ const preview = text.length > FIELD_TEXT_PREVIEW_MAX_CHARS ? text.slice(-FIELD_TEXT_PREVIEW_MAX_CHARS) : text;
154
+ this._pushFieldEvent({ type: "blur", fieldId, valuePreview: preview, valueLength: text.length, timestamp: ts });
155
+ }
156
+ // ─── Significance gate ───
157
+ // Fires at most once per `minSuggestIntervalMs`, only after `idleThresholdMs`
158
+ // of inactivity following new activity — never on every keystroke.
159
+ checkSignificance(ts = now()) {
160
+ if (this.fieldHistory.length < this.minEventsForSuggestion) return false;
161
+ if (this.lastActivityAt <= this.lastSuggestedAt) return false;
162
+ if (ts - this.lastActivityAt < this.idleThresholdMs) return false;
163
+ if (ts - this.lastSuggestedAt < this.minSuggestIntervalMs) return false;
164
+ this.lastSuggestedAt = ts;
165
+ return true;
166
+ }
167
+ // ─── Prompt builder ───
168
+ // Compact natural-language description of recent field activity, oldest first.
169
+ buildPrompt() {
170
+ if (this.fieldHistory.length === 0) return null;
171
+ const lines = this.fieldHistory.map((e) => {
172
+ if (e.type === "focus") {
173
+ return e.revisit ? `User returned to field "${e.fieldId}".` : `User focused field "${e.fieldId}".`;
174
+ }
175
+ if (e.valueLength === 0) return `User left field "${e.fieldId}" empty.`;
176
+ return `Field "${e.fieldId}" now contains: "${e.valuePreview}"`;
177
+ });
178
+ return [
179
+ "You are an ambient UX assistant embedded in a form.",
180
+ "Recent user activity, oldest first:",
181
+ ...lines,
182
+ "",
183
+ "Based only on this activity, suggest exactly one concise, specific next-step action the user might want.",
184
+ 'Respond as compact JSON only: {"label": string (<=60 chars), "action": string (<=140 chars), "confidence": number 0-1}.',
185
+ 'If nothing useful can be suggested, respond {"label": null}.'
186
+ ].join("\n");
187
+ }
188
+ };
189
+ function createSentinel(name, options) {
190
+ return new SolaSentinel(name, options);
191
+ }
192
+
193
+ // src/index.js
43
194
  var effectStack = [];
44
195
  var pendingEffects = /* @__PURE__ */ new Set();
45
196
  var isFlushing = false;
@@ -442,62 +593,5 @@ var SolaCore = (() => {
442
593
  };
443
594
  var signalMesh = new SignalMeshEngine();
444
595
  var createTopicSignal = (topic, initialVal) => signalMesh.topic(topic, initialVal);
445
- var SolaSentinel = class {
446
- constructor(name = "default", options = {}) {
447
- this.name = name;
448
- this.thresholdMs = options.thresholdMs || 600;
449
- this.maxRageClicks = options.maxRageClicks || 3;
450
- this.clickHistory = [];
451
- this.subscribers = /* @__PURE__ */ new Set();
452
- this.frictionEvents = [];
453
- this.flowIndex = 99.8;
454
- }
455
- recordClick(actionId, target = "button") {
456
- const now = typeof performance !== "undefined" ? performance.now() : Date.now();
457
- this.clickHistory.push({ actionId, target, timestamp: now });
458
- this.clickHistory = this.clickHistory.filter((c) => now - c.timestamp < 2e3);
459
- const recent = this.clickHistory.filter((c) => c.actionId === actionId && now - c.timestamp < this.thresholdMs);
460
- if (recent.length >= this.maxRageClicks) {
461
- this.triggerFrictionAlert({
462
- type: "RAGE_CLICK",
463
- actionId,
464
- target,
465
- count: recent.length,
466
- timestamp: now,
467
- severity: "HIGH",
468
- message: `Rage-click burst: ${recent.length} taps in ${Math.round(now - recent[0].timestamp)}ms`
469
- });
470
- }
471
- }
472
- recordSignalDrop(topic, error) {
473
- this.triggerFrictionAlert({
474
- type: "SIGNAL_TIMEOUT",
475
- topic,
476
- error: error?.message || String(error),
477
- timestamp: typeof performance !== "undefined" ? performance.now() : Date.now(),
478
- severity: "CRITICAL",
479
- message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
480
- });
481
- }
482
- triggerFrictionAlert(event) {
483
- this.frictionEvents.unshift(event);
484
- if (this.frictionEvents.length > 50) this.frictionEvents.pop();
485
- this.flowIndex = Math.max(68.5, Number((this.flowIndex - 3.8).toFixed(1)));
486
- this.subscribers.forEach((cb) => {
487
- try {
488
- cb(event, this);
489
- } catch (e) {
490
- console.error(e);
491
- }
492
- });
493
- }
494
- onFriction(cb) {
495
- this.subscribers.add(cb);
496
- return () => this.subscribers.delete(cb);
497
- }
498
- };
499
- function createSentinel(name, options) {
500
- return new SolaSentinel(name, options);
501
- }
502
596
  return __toCommonJS(src_exports);
503
597
  })();
@@ -1,3 +1,4 @@
1
1
  /* @sola-air-ui/core v1.0.2 | MIT */
2
- var SolaCore=(()=>{var v=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var H=Object.getOwnPropertyNames;var R=Object.prototype.hasOwnProperty;var $=(r,e)=>{for(var t in e)v(r,t,{get:e[t],enumerable:!0})},N=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of H(e))!R.call(r,s)&&s!==t&&v(r,s,{get:()=>e[s],enumerable:!(n=O(e,s))||n.enumerable});return r};var F=r=>N(v({},"__esModule",{value:!0}),r);var ee={};$(ee,{SolaSentinel:()=>S,__flush_destroys:()=>P,__flush_mounts:()=>J,configureData:()=>B,configureIntent:()=>z,createData:()=>X,createDerived:()=>L,createEffect:()=>M,createIntent:()=>U,createSentinel:()=>Z,createSignal:()=>b,createTopicSignal:()=>Y,flushSync:()=>D,onDestroy:()=>T,onMount:()=>G,popContext:()=>q,pushContext:()=>j,signalMesh:()=>_});var p=[],w=new Set,E=!1;function D(){for(;w.size>0;){let r=[...w];w.clear();for(let e of r)e.execute()}E=!1}function A(){E||(E=!0,queueMicrotask(D))}function b(r){let e=r,t=new Set;return[()=>{let o=p[p.length-1];return o&&(t.add(o),o.dependencies.add(t)),e},o=>{if(e!==o){e=o;for(let i of[...t])w.add(i);A()}}]}function M(r){let e={execute(){t(),p.push(e);try{r()}finally{p.pop()}},dependencies:new Set,cleanup:t};function t(){for(let n of e.dependencies)n.delete(e);e.dependencies.clear()}p.push(e);try{r()}finally{p.pop()}return t}function L(r){let e,t=!0,n=new Set,s=new Set,o={execute(){if(!t){t=!0;for(let c of[...n])w.add(c);A()}},dependencies:s,cleanup(){for(let c of s)c.delete(o);s.clear()}};return()=>{let c=p[p.length-1];if(c&&(n.add(c),c.dependencies.add(n)),t){o.cleanup(),s=new Set,o.dependencies=s,p.push(o);try{e=r()}finally{p.pop()}t=!1}return e}}var m=[],u=null;function j(){let r={mounts:[],destroys:[]};return m.push(r),u=r,r}function q(r){let e=m.lastIndexOf(r);e!==-1&&m.splice(e,1),u=m.length>0?m[m.length-1]:null}function G(r){u?u.mounts.push(r):r()}function T(r){u&&u.destroys.push(r)}function J(){if(u&&u.mounts.length>0){let r=[...u.mounts];u.mounts=[];for(let e of r)e()}}function P(){if(u&&u.destroys.length>0){let r=[...u.destroys];u.destroys=[];for(let e of r)e()}}var W={provider:"local",endpoint:"/api/intent",model:"gemini-2.5-flash",stream:!1},k={...W};function z(r){k={...k,...r}}async function K(r,e,t,n){let s=r.body.getReader(),o=new TextDecoder,i="";try{for(;;){let{done:c,value:f}=await s.read();if(c)break;i+=o.decode(f,{stream:!0});let a=i.split(`
3
- `);i=a.pop();for(let l of a){if(!l.startsWith("data: "))continue;let d=l.slice(6).trim();if(d==="[DONE]"){t();return}try{let g=JSON.parse(d),h=g.token??g.delta??g.content??"";h&&e(h)}catch{d&&e(d)}}}t()}catch(c){c.name!=="AbortError"&&n(c)}}function U(r,e={}){let t={...k,...e},[n,s]=b(e.initial??null),[o,i]=b(!1),[c,f]=b(null),a=null;T(()=>{a&&a.abort()}),M(()=>{let d=typeof r=="function"?r():r;if(!d)return;a&&a.abort(),a=new AbortController,s(null),f(null),i(!0);let g=JSON.stringify({messages:[{role:"user",content:d}],model:t.model,provider:t.provider,stream:t.stream});fetch(t.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:g,signal:a.signal}).then(h=>{if(!h.ok)throw new Error(`Intent failed: ${h.status}`);if(t.stream){let y="";return K(h,x=>{y+=x,s(y)},()=>i(!1),x=>{f(x.message),i(!1)})}return h.json().then(y=>{y?.components?.length>0?s(y.components[0]):y?.result!=null?s(y.result):s(y),i(!1)})}).catch(h=>{h.name!=="AbortError"&&(console.error("[Sola Intent Error]",h),f(h.message),i(!1))})});let l=n;return l.loading=o,l.error=c,l}var V={relayEndpoint:"http://localhost:4040/api/query",refresh:null},C={...V};function B(r){C={...C,...r}}function Q(r){if(!r)return null;let e=r.match(/^(\d+)(s|m|h)$/);if(!e)return null;let t=parseInt(e[1]);switch(e[2]){case"s":return t*1e3;case"m":return t*60*1e3;case"h":return t*3600*1e3}return null}function X(r,e={}){let t={...C,...e},[n,s]=b({loading:!0,data:null,error:null}),o=null,i=null;function c(){o&&o.abort(),o=new AbortController,s({loading:!0,data:n().data,error:null}),fetch(t.relayEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source:r,query:t.query||null,filters:t.filters||null,sort:t.sort||null,limit:t.limit||null,offset:t.offset||null}),signal:o.signal}).then(l=>{if(!l.ok)throw new Error(`Data fetch failed: ${l.status}`);return l.json()}).then(l=>{s({loading:!1,data:l.rows||l,error:null})}).catch(l=>{l.name!=="AbortError"&&(console.error("[Sola Data Error]",l),s({loading:!1,data:null,error:l.message}))})}c();let f=Q(t.refresh);f&&(i=setInterval(c,f));let a=()=>n();return a.refetch=c,a.stop=()=>{i&&clearInterval(i),o&&o.abort()},a}var I=class{constructor(){this.topics=new Map,this.telemetrySubscribers=new Set,this.cycleStack=new Set}topic(e,t){if(!this.topics.has(e)){let[i,c]=b(t);this.topics.set(e,{read:i,write:c,value:t,subscribers:new Set})}let n=this.topics.get(e);return[()=>n.read(),(i,c="signal")=>{let f=typeof i=="function"?i(n.value):i;if(n.value===f)return;if(this.cycleStack.has(e)){console.warn(`[Sola Signal Mesh] Cycle detected on topic "${e}". Aborting cyclic dispatch.`);return}let a=n.value;n.value=f,n.write(f);let l={topic:e,value:f,prevValue:a,timestamp:typeof performance<"u"?performance.now():Date.now(),originWidgetId:c};this.telemetrySubscribers.forEach(d=>{try{d(l)}catch(g){console.error(g)}}),this.cycleStack.add(e);try{n.subscribers.forEach(d=>{try{d(f,l)}catch(g){console.error(g)}})}finally{this.cycleStack.delete(e)}}]}subscribe(e,t){this.topics.has(e)||this.topic(e,void 0);let n=this.topics.get(e);return n.subscribers.add(t),()=>n.subscribers.delete(t)}onTelemetry(e){return this.telemetrySubscribers.add(e),()=>this.telemetrySubscribers.delete(e)}},_=new I,Y=(r,e)=>_.topic(r,e),S=class{constructor(e="default",t={}){this.name=e,this.thresholdMs=t.thresholdMs||600,this.maxRageClicks=t.maxRageClicks||3,this.clickHistory=[],this.subscribers=new Set,this.frictionEvents=[],this.flowIndex=99.8}recordClick(e,t="button"){let n=typeof performance<"u"?performance.now():Date.now();this.clickHistory.push({actionId:e,target:t,timestamp:n}),this.clickHistory=this.clickHistory.filter(o=>n-o.timestamp<2e3);let s=this.clickHistory.filter(o=>o.actionId===e&&n-o.timestamp<this.thresholdMs);s.length>=this.maxRageClicks&&this.triggerFrictionAlert({type:"RAGE_CLICK",actionId:e,target:t,count:s.length,timestamp:n,severity:"HIGH",message:`Rage-click burst: ${s.length} taps in ${Math.round(n-s[0].timestamp)}ms`})}recordSignalDrop(e,t){this.triggerFrictionAlert({type:"SIGNAL_TIMEOUT",topic:e,error:t?.message||String(t),timestamp:typeof performance<"u"?performance.now():Date.now(),severity:"CRITICAL",message:`Signal channel "${e}" breached SLA timeout (504 Gateway Stall)`})}triggerFrictionAlert(e){this.frictionEvents.unshift(e),this.frictionEvents.length>50&&this.frictionEvents.pop(),this.flowIndex=Math.max(68.5,Number((this.flowIndex-3.8).toFixed(1))),this.subscribers.forEach(t=>{try{t(e,this)}catch(n){console.error(n)}})}onFriction(e){return this.subscribers.add(e),()=>this.subscribers.delete(e)}};function Z(r,e){return new S(r,e)}return F(ee);})();
2
+ var SolaCore=(()=>{var I=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var L=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var N=(s,t)=>{for(var e in t)I(s,e,{get:t[e],enumerable:!0})},O=(s,t,e,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of L(t))!$.call(s,i)&&i!==e&&I(s,i,{get:()=>t[i],enumerable:!(n=R(t,i))||n.enumerable});return s};var W=s=>O(I({},"__esModule",{value:!0}),s);var et={};N(et,{SolaSentinel:()=>E,__flush_destroys:()=>j,__flush_mounts:()=>V,configureData:()=>Y,configureIntent:()=>G,createData:()=>Z,createDerived:()=>U,createEffect:()=>D,createIntent:()=>z,createSentinel:()=>C,createSignal:()=>S,createTopicSignal:()=>tt,flushSync:()=>M,onDestroy:()=>k,onMount:()=>B,popContext:()=>P,pushContext:()=>X,signalMesh:()=>T});function y(){return typeof performance<"u"?performance.now():Date.now()}var E=class{constructor(t="default",e={}){this.name=t,this.thresholdMs=e.thresholdMs||600,this.maxRageClicks=e.maxRageClicks||3,this.clickHistory=[],this.subscribers=new Set,this.frictionEvents=[],this.flowIndex=99.8,this.fieldHistory=[],this.lastActivityAt=0,this.lastSuggestedAt=-1/0,this.idleThresholdMs=e.idleThresholdMs??1500,this.minSuggestIntervalMs=e.minSuggestIntervalMs??8e3,this.minEventsForSuggestion=e.minEventsForSuggestion??2}recordClick(t,e="button"){let n=y();this.clickHistory.push({actionId:t,target:e,timestamp:n}),this.clickHistory=this.clickHistory.filter(r=>n-r.timestamp<2e3);let i=this.clickHistory.filter(r=>r.actionId===t&&n-r.timestamp<this.thresholdMs);i.length>=this.maxRageClicks&&this.triggerFrictionAlert({type:"RAGE_CLICK",actionId:t,target:e,count:i.length,timestamp:n,severity:"HIGH",message:`Rage-click burst: ${i.length} taps in ${Math.round(n-i[0].timestamp)}ms`})}recordSignalDrop(t,e){this.triggerFrictionAlert({type:"SIGNAL_TIMEOUT",topic:t,error:e?.message||String(e),timestamp:y(),severity:"CRITICAL",message:`Signal channel "${t}" breached SLA timeout (504 Gateway Stall)`})}triggerFrictionAlert(t){this.frictionEvents.unshift(t),this.frictionEvents.length>50&&this.frictionEvents.pop(),this._recomputeFlowIndex(t.timestamp),this.subscribers.forEach(e=>{try{e(t,this)}catch(n){console.error(n)}})}onFriction(t){return this.subscribers.add(t),()=>this.subscribers.delete(t)}_recomputeFlowIndex(t=y()){let e=99.8,n=this.frictionEvents.filter(r=>t-r.timestamp<12e4);e-=n.reduce((r,o)=>{let c=o.severity==="CRITICAL"?6:o.severity==="HIGH"?3.8:2,l=Math.max(.3,1-(t-o.timestamp)/12e4);return r+c*l},0);let i=this.fieldHistory.filter(r=>r.type==="focus");if(i.length>0){let r=i.filter(o=>o.revisit).length/i.length;e-=r*15}if(this.fieldHistory.length>=3){let r=[];for(let l=1;l<this.fieldHistory.length;l++)r.push(this.fieldHistory[l].timestamp-this.fieldHistory[l-1].timestamp);let o=r.reduce((l,u)=>l+u,0)/r.length,c=r.reduce((l,u)=>l+(u-o)**2,0)/r.length;e-=Math.min(10,Math.sqrt(c)/500)}return this.flowIndex=Math.max(0,Math.min(99.8,Number(e.toFixed(1)))),this.flowIndex}_pushFieldEvent(t){this.fieldHistory.push(t),this.fieldHistory=this.fieldHistory.filter(e=>t.timestamp-e.timestamp<6e4).slice(-50),this.lastActivityAt=t.timestamp,this._recomputeFlowIndex(t.timestamp)}recordFieldFocus(t,e=y()){let n=this.fieldHistory.some(i=>i.type==="blur"&&i.fieldId===t);return this._pushFieldEvent({type:"focus",fieldId:t,revisit:n,timestamp:e}),n}recordFieldBlur(t,e,n=y()){let i=String(e??""),r=i.length>200?i.slice(-200):i;this._pushFieldEvent({type:"blur",fieldId:t,valuePreview:r,valueLength:i.length,timestamp:n})}checkSignificance(t=y()){return this.fieldHistory.length<this.minEventsForSuggestion||this.lastActivityAt<=this.lastSuggestedAt||t-this.lastActivityAt<this.idleThresholdMs||t-this.lastSuggestedAt<this.minSuggestIntervalMs?!1:(this.lastSuggestedAt=t,!0)}buildPrompt(){return this.fieldHistory.length===0?null:["You are an ambient UX assistant embedded in a form.","Recent user activity, oldest first:",...this.fieldHistory.map(e=>e.type==="focus"?e.revisit?`User returned to field "${e.fieldId}".`:`User focused field "${e.fieldId}".`:e.valueLength===0?`User left field "${e.fieldId}" empty.`:`Field "${e.fieldId}" now contains: "${e.valuePreview}"`),"","Based only on this activity, suggest exactly one concise, specific next-step action the user might want.",'Respond as compact JSON only: {"label": string (<=60 chars), "action": string (<=140 chars), "confidence": number 0-1}.','If nothing useful can be suggested, respond {"label": null}.'].join(`
3
+ `)}};function C(s,t){return new E(s,t)}var p=[],v=new Set,x=!1;function M(){for(;v.size>0;){let s=[...v];v.clear();for(let t of s)t.execute()}x=!1}function H(){x||(x=!0,queueMicrotask(M))}function S(s){let t=s,e=new Set;return[()=>{let r=p[p.length-1];return r&&(e.add(r),r.dependencies.add(e)),t},r=>{if(t!==r){t=r;for(let o of[...e])v.add(o);H()}}]}function D(s){let t={execute(){e(),p.push(t);try{s()}finally{p.pop()}},dependencies:new Set,cleanup:e};function e(){for(let n of t.dependencies)n.delete(t);t.dependencies.clear()}p.push(t);try{s()}finally{p.pop()}return e}function U(s){let t,e=!0,n=new Set,i=new Set,r={execute(){if(!e){e=!0;for(let c of[...n])v.add(c);H()}},dependencies:i,cleanup(){for(let c of i)c.delete(r);i.clear()}};return()=>{let c=p[p.length-1];if(c&&(n.add(c),c.dependencies.add(n)),e){r.cleanup(),i=new Set,r.dependencies=i,p.push(r);try{t=s()}finally{p.pop()}e=!1}return t}}var b=[],f=null;function X(){let s={mounts:[],destroys:[]};return b.push(s),f=s,s}function P(s){let t=b.lastIndexOf(s);t!==-1&&b.splice(t,1),f=b.length>0?b[b.length-1]:null}function B(s){f?f.mounts.push(s):s()}function k(s){f&&f.destroys.push(s)}function V(){if(f&&f.mounts.length>0){let s=[...f.mounts];f.mounts=[];for(let t of s)t()}}function j(){if(f&&f.destroys.length>0){let s=[...f.destroys];f.destroys=[];for(let t of s)t()}}var q={provider:"local",endpoint:"/api/intent",model:"gemini-2.5-flash",stream:!1},_={...q};function G(s){_={..._,...s}}async function J(s,t,e,n){let i=s.body.getReader(),r=new TextDecoder,o="";try{for(;;){let{done:c,value:l}=await i.read();if(c)break;o+=r.decode(l,{stream:!0});let u=o.split(`
4
+ `);o=u.pop();for(let a of u){if(!a.startsWith("data: "))continue;let d=a.slice(6).trim();if(d==="[DONE]"){e();return}try{let g=JSON.parse(d),h=g.token??g.delta??g.content??"";h&&t(h)}catch{d&&t(d)}}}e()}catch(c){c.name!=="AbortError"&&n(c)}}function z(s,t={}){let e={..._,...t},[n,i]=S(t.initial??null),[r,o]=S(!1),[c,l]=S(null),u=null;k(()=>{u&&u.abort()}),D(()=>{let d=typeof s=="function"?s():s;if(!d)return;u&&u.abort(),u=new AbortController,i(null),l(null),o(!0);let g=JSON.stringify({messages:[{role:"user",content:d}],model:e.model,provider:e.provider,stream:e.stream});fetch(e.endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:g,signal:u.signal}).then(h=>{if(!h.ok)throw new Error(`Intent failed: ${h.status}`);if(e.stream){let m="";return J(h,w=>{m+=w,i(m)},()=>o(!1),w=>{l(w.message),o(!1)})}return h.json().then(m=>{m?.components?.length>0?i(m.components[0]):m?.result!=null?i(m.result):i(m),o(!1)})}).catch(h=>{h.name!=="AbortError"&&(console.error("[Sola Intent Error]",h),l(h.message),o(!1))})});let a=n;return a.loading=r,a.error=c,a}var K={relayEndpoint:"http://localhost:4040/api/query",refresh:null},A={...K};function Y(s){A={...A,...s}}function Q(s){if(!s)return null;let t=s.match(/^(\d+)(s|m|h)$/);if(!t)return null;let e=parseInt(t[1]);switch(t[2]){case"s":return e*1e3;case"m":return e*60*1e3;case"h":return e*3600*1e3}return null}function Z(s,t={}){let e={...A,...t},[n,i]=S({loading:!0,data:null,error:null}),r=null,o=null;function c(){r&&r.abort(),r=new AbortController,i({loading:!0,data:n().data,error:null}),fetch(e.relayEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source:s,query:e.query||null,filters:e.filters||null,sort:e.sort||null,limit:e.limit||null,offset:e.offset||null}),signal:r.signal}).then(a=>{if(!a.ok)throw new Error(`Data fetch failed: ${a.status}`);return a.json()}).then(a=>{i({loading:!1,data:a.rows||a,error:null})}).catch(a=>{a.name!=="AbortError"&&(console.error("[Sola Data Error]",a),i({loading:!1,data:null,error:a.message}))})}c();let l=Q(e.refresh);l&&(o=setInterval(c,l));let u=()=>n();return u.refetch=c,u.stop=()=>{o&&clearInterval(o),r&&r.abort()},u}var F=class{constructor(){this.topics=new Map,this.telemetrySubscribers=new Set,this.cycleStack=new Set}topic(t,e){if(!this.topics.has(t)){let[o,c]=S(e);this.topics.set(t,{read:o,write:c,value:e,subscribers:new Set})}let n=this.topics.get(t);return[()=>n.read(),(o,c="signal")=>{let l=typeof o=="function"?o(n.value):o;if(n.value===l)return;if(this.cycleStack.has(t)){console.warn(`[Sola Signal Mesh] Cycle detected on topic "${t}". Aborting cyclic dispatch.`);return}let u=n.value;n.value=l,n.write(l);let a={topic:t,value:l,prevValue:u,timestamp:typeof performance<"u"?performance.now():Date.now(),originWidgetId:c};this.telemetrySubscribers.forEach(d=>{try{d(a)}catch(g){console.error(g)}}),this.cycleStack.add(t);try{n.subscribers.forEach(d=>{try{d(l,a)}catch(g){console.error(g)}})}finally{this.cycleStack.delete(t)}}]}subscribe(t,e){this.topics.has(t)||this.topic(t,void 0);let n=this.topics.get(t);return n.subscribers.add(e),()=>n.subscribers.delete(e)}onTelemetry(t){return this.telemetrySubscribers.add(t),()=>this.telemetrySubscribers.delete(t)}},T=new F,tt=(s,t)=>T.topic(s,t);return W(et);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sola-air-ui/core",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-VDOM reactivity engine — signals, effects, lifecycle, and intent primitives",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.js CHANGED
@@ -476,65 +476,8 @@ class SignalMeshEngine {
476
476
  export const signalMesh = new SignalMeshEngine();
477
477
  export const createTopicSignal = (topic, initialVal) => signalMesh.topic(topic, initialVal);
478
478
 
479
- // ─── Sola Sentinel & Intent Telemetry Observer ───
480
- export class SolaSentinel {
481
- constructor(name = 'default', options = {}) {
482
- this.name = name;
483
- this.thresholdMs = options.thresholdMs || 600;
484
- this.maxRageClicks = options.maxRageClicks || 3;
485
- this.clickHistory = [];
486
- this.subscribers = new Set();
487
- this.frictionEvents = [];
488
- this.flowIndex = 99.8;
489
- }
490
-
491
- recordClick(actionId, target = 'button') {
492
- const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
493
- this.clickHistory.push({ actionId, target, timestamp: now });
494
- this.clickHistory = this.clickHistory.filter(c => now - c.timestamp < 2000);
495
-
496
- const recent = this.clickHistory.filter(c => c.actionId === actionId && now - c.timestamp < this.thresholdMs);
497
- if (recent.length >= this.maxRageClicks) {
498
- this.triggerFrictionAlert({
499
- type: 'RAGE_CLICK',
500
- actionId,
501
- target,
502
- count: recent.length,
503
- timestamp: now,
504
- severity: 'HIGH',
505
- message: `Rage-click burst: ${recent.length} taps in ${Math.round(now - recent[0].timestamp)}ms`
506
- });
507
- }
508
- }
509
-
510
- recordSignalDrop(topic, error) {
511
- this.triggerFrictionAlert({
512
- type: 'SIGNAL_TIMEOUT',
513
- topic,
514
- error: error?.message || String(error),
515
- timestamp: typeof performance !== 'undefined' ? performance.now() : Date.now(),
516
- severity: 'CRITICAL',
517
- message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
518
- });
519
- }
520
-
521
- triggerFrictionAlert(event) {
522
- this.frictionEvents.unshift(event);
523
- if (this.frictionEvents.length > 50) this.frictionEvents.pop();
524
- this.flowIndex = Math.max(68.5, Number((this.flowIndex - 3.8).toFixed(1)));
525
-
526
- this.subscribers.forEach(cb => {
527
- try { cb(event, this); } catch(e) { console.error(e); }
528
- });
529
- }
530
-
531
- onFriction(cb) {
532
- this.subscribers.add(cb);
533
- return () => this.subscribers.delete(cb);
534
- }
535
- }
536
-
537
- export function createSentinel(name, options) {
538
- return new SolaSentinel(name, options);
539
- }
479
+ // ─── Sola Sentinel & Ambient Intent Telemetry Observer ───
480
+ // Moved to sentinel.js — friction/rage-click detection, plus ambient
481
+ // field-behavior capture, significance gating, and prompt building.
482
+ export { SolaSentinel, createSentinel } from './sentinel.js';
540
483
 
@@ -0,0 +1,179 @@
1
+ // ─── Sola Sentinel & Ambient Intent Telemetry Observer ───
2
+ // Rage-click / signal-drop friction detection, plus ambient field-level
3
+ // behavior capture (focus, revisit, blur-with-value) feeding a debounced
4
+ // significance gate and a prompt builder for $intent-driven suggestions.
5
+
6
+ const FIELD_BUFFER_MAX_EVENTS = 50;
7
+ const FIELD_BUFFER_WINDOW_MS = 60_000;
8
+ const FIELD_TEXT_PREVIEW_MAX_CHARS = 200;
9
+
10
+ function now() {
11
+ return typeof performance !== 'undefined' ? performance.now() : Date.now();
12
+ }
13
+
14
+ export class SolaSentinel {
15
+ constructor(name = 'default', options = {}) {
16
+ this.name = name;
17
+ this.thresholdMs = options.thresholdMs || 600;
18
+ this.maxRageClicks = options.maxRageClicks || 3;
19
+ this.clickHistory = [];
20
+ this.subscribers = new Set();
21
+ this.frictionEvents = [];
22
+ this.flowIndex = 99.8;
23
+
24
+ // Ambient field-behavior observation
25
+ this.fieldHistory = [];
26
+ this.lastActivityAt = 0;
27
+ this.lastSuggestedAt = -Infinity;
28
+ this.idleThresholdMs = options.idleThresholdMs ?? 1500;
29
+ this.minSuggestIntervalMs = options.minSuggestIntervalMs ?? 8000;
30
+ this.minEventsForSuggestion = options.minEventsForSuggestion ?? 2;
31
+ }
32
+
33
+ recordClick(actionId, target = 'button') {
34
+ const ts = now();
35
+ this.clickHistory.push({ actionId, target, timestamp: ts });
36
+ this.clickHistory = this.clickHistory.filter(c => ts - c.timestamp < 2000);
37
+
38
+ const recent = this.clickHistory.filter(c => c.actionId === actionId && ts - c.timestamp < this.thresholdMs);
39
+ if (recent.length >= this.maxRageClicks) {
40
+ this.triggerFrictionAlert({
41
+ type: 'RAGE_CLICK',
42
+ actionId,
43
+ target,
44
+ count: recent.length,
45
+ timestamp: ts,
46
+ severity: 'HIGH',
47
+ message: `Rage-click burst: ${recent.length} taps in ${Math.round(ts - recent[0].timestamp)}ms`
48
+ });
49
+ }
50
+ }
51
+
52
+ recordSignalDrop(topic, error) {
53
+ this.triggerFrictionAlert({
54
+ type: 'SIGNAL_TIMEOUT',
55
+ topic,
56
+ error: error?.message || String(error),
57
+ timestamp: now(),
58
+ severity: 'CRITICAL',
59
+ message: `Signal channel "${topic}" breached SLA timeout (504 Gateway Stall)`
60
+ });
61
+ }
62
+
63
+ triggerFrictionAlert(event) {
64
+ this.frictionEvents.unshift(event);
65
+ if (this.frictionEvents.length > 50) this.frictionEvents.pop();
66
+ this._recomputeFlowIndex(event.timestamp);
67
+
68
+ this.subscribers.forEach(cb => {
69
+ try { cb(event, this); } catch (e) { console.error(e); }
70
+ });
71
+ }
72
+
73
+ onFriction(cb) {
74
+ this.subscribers.add(cb);
75
+ return () => this.subscribers.delete(cb);
76
+ }
77
+
78
+ // ─── Flow index ───
79
+ // A real computed score, not a fixed decrement: severity- and recency-weighted
80
+ // friction events, the share of field visits that were backtracks, plus how
81
+ // erratic the pacing between field events is (a proxy for hesitation).
82
+ _recomputeFlowIndex(ts = now()) {
83
+ let score = 99.8;
84
+
85
+ const recentFriction = this.frictionEvents.filter(e => ts - e.timestamp < 120_000);
86
+ score -= recentFriction.reduce((sum, e) => {
87
+ const severityWeight = e.severity === 'CRITICAL' ? 6 : e.severity === 'HIGH' ? 3.8 : 2;
88
+ const recencyWeight = Math.max(0.3, 1 - (ts - e.timestamp) / 120_000);
89
+ return sum + severityWeight * recencyWeight;
90
+ }, 0);
91
+
92
+ const focusEvents = this.fieldHistory.filter(e => e.type === 'focus');
93
+ if (focusEvents.length > 0) {
94
+ const revisitRatio = focusEvents.filter(e => e.revisit).length / focusEvents.length;
95
+ score -= revisitRatio * 15;
96
+ }
97
+
98
+ if (this.fieldHistory.length >= 3) {
99
+ const gaps = [];
100
+ for (let i = 1; i < this.fieldHistory.length; i++) {
101
+ gaps.push(this.fieldHistory[i].timestamp - this.fieldHistory[i - 1].timestamp);
102
+ }
103
+ const mean = gaps.reduce((a, b) => a + b, 0) / gaps.length;
104
+ const variance = gaps.reduce((sum, g) => sum + (g - mean) ** 2, 0) / gaps.length;
105
+ score -= Math.min(10, Math.sqrt(variance) / 500);
106
+ }
107
+
108
+ this.flowIndex = Math.max(0, Math.min(99.8, Number(score.toFixed(1))));
109
+ return this.flowIndex;
110
+ }
111
+
112
+ // ─── Ambient field observation ───
113
+
114
+ _pushFieldEvent(event) {
115
+ this.fieldHistory.push(event);
116
+ this.fieldHistory = this.fieldHistory
117
+ .filter(e => event.timestamp - e.timestamp < FIELD_BUFFER_WINDOW_MS)
118
+ .slice(-FIELD_BUFFER_MAX_EVENTS);
119
+ this.lastActivityAt = event.timestamp;
120
+ this._recomputeFlowIndex(event.timestamp);
121
+ }
122
+
123
+ recordFieldFocus(fieldId, ts = now()) {
124
+ const revisit = this.fieldHistory.some(e => e.type === 'blur' && e.fieldId === fieldId);
125
+ this._pushFieldEvent({ type: 'focus', fieldId, revisit, timestamp: ts });
126
+ return revisit;
127
+ }
128
+
129
+ recordFieldBlur(fieldId, value, ts = now()) {
130
+ const text = String(value ?? '');
131
+ const preview = text.length > FIELD_TEXT_PREVIEW_MAX_CHARS
132
+ ? text.slice(-FIELD_TEXT_PREVIEW_MAX_CHARS)
133
+ : text;
134
+ this._pushFieldEvent({ type: 'blur', fieldId, valuePreview: preview, valueLength: text.length, timestamp: ts });
135
+ }
136
+
137
+ // ─── Significance gate ───
138
+ // Fires at most once per `minSuggestIntervalMs`, only after `idleThresholdMs`
139
+ // of inactivity following new activity — never on every keystroke.
140
+ checkSignificance(ts = now()) {
141
+ if (this.fieldHistory.length < this.minEventsForSuggestion) return false;
142
+ if (this.lastActivityAt <= this.lastSuggestedAt) return false;
143
+ if (ts - this.lastActivityAt < this.idleThresholdMs) return false;
144
+ if (ts - this.lastSuggestedAt < this.minSuggestIntervalMs) return false;
145
+
146
+ this.lastSuggestedAt = ts;
147
+ return true;
148
+ }
149
+
150
+ // ─── Prompt builder ───
151
+ // Compact natural-language description of recent field activity, oldest first.
152
+ buildPrompt() {
153
+ if (this.fieldHistory.length === 0) return null;
154
+
155
+ const lines = this.fieldHistory.map(e => {
156
+ if (e.type === 'focus') {
157
+ return e.revisit
158
+ ? `User returned to field "${e.fieldId}".`
159
+ : `User focused field "${e.fieldId}".`;
160
+ }
161
+ if (e.valueLength === 0) return `User left field "${e.fieldId}" empty.`;
162
+ return `Field "${e.fieldId}" now contains: "${e.valuePreview}"`;
163
+ });
164
+
165
+ return [
166
+ 'You are an ambient UX assistant embedded in a form.',
167
+ 'Recent user activity, oldest first:',
168
+ ...lines,
169
+ '',
170
+ 'Based only on this activity, suggest exactly one concise, specific next-step action the user might want.',
171
+ 'Respond as compact JSON only: {"label": string (<=60 chars), "action": string (<=140 chars), "confidence": number 0-1}.',
172
+ 'If nothing useful can be suggested, respond {"label": null}.'
173
+ ].join('\n');
174
+ }
175
+ }
176
+
177
+ export function createSentinel(name, options) {
178
+ return new SolaSentinel(name, options);
179
+ }