sgiant-ai-widget 1.0.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 +21 -0
- package/README.md +155 -0
- package/dist/ai-invalidation.d.ts +29 -0
- package/dist/ai-invalidation.d.ts.map +1 -0
- package/dist/ai-invalidation.js +121 -0
- package/dist/ai-invalidation.js.map +1 -0
- package/dist/contrast.d.ts +87 -0
- package/dist/contrast.d.ts.map +1 -0
- package/dist/contrast.js +151 -0
- package/dist/contrast.js.map +1 -0
- package/dist/directive.d.ts +37 -0
- package/dist/directive.d.ts.map +1 -0
- package/dist/directive.js +87 -0
- package/dist/directive.js.map +1 -0
- package/dist/dom.d.ts +83 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +146 -0
- package/dist/dom.js.map +1 -0
- package/dist/host-actions.d.ts +192 -0
- package/dist/host-actions.d.ts.map +1 -0
- package/dist/host-actions.js +443 -0
- package/dist/host-actions.js.map +1 -0
- package/dist/icons.d.ts +21 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +84 -0
- package/dist/icons.js.map +1 -0
- package/dist/index.d.ts +600 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4833 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.d.ts +187 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +232 -0
- package/dist/labels.js.map +1 -0
- package/dist/limits.d.ts +43 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +48 -0
- package/dist/limits.js.map +1 -0
- package/dist/markdown.d.ts +23 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +0 -0
- package/dist/markdown.js.map +1 -0
- package/dist/message-chrome.d.ts +45 -0
- package/dist/message-chrome.d.ts.map +1 -0
- package/dist/message-chrome.js +101 -0
- package/dist/message-chrome.js.map +1 -0
- package/dist/pane-follow.d.ts +67 -0
- package/dist/pane-follow.d.ts.map +1 -0
- package/dist/pane-follow.js +61 -0
- package/dist/pane-follow.js.map +1 -0
- package/dist/prefix.d.ts +9 -0
- package/dist/prefix.d.ts.map +1 -0
- package/dist/prefix.js +9 -0
- package/dist/prefix.js.map +1 -0
- package/dist/proposal-summary.d.ts +19 -0
- package/dist/proposal-summary.d.ts.map +1 -0
- package/dist/proposal-summary.js +73 -0
- package/dist/proposal-summary.js.map +1 -0
- package/dist/replay.d.ts +125 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +172 -0
- package/dist/replay.js.map +1 -0
- package/dist/safe-url.d.ts +47 -0
- package/dist/safe-url.d.ts.map +1 -0
- package/dist/safe-url.js +80 -0
- package/dist/safe-url.js.map +1 -0
- package/dist/sgiant-ai-widget.global.js +778 -0
- package/dist/sgiant-ai-widget.global.js.map +7 -0
- package/dist/specs.d.ts +173 -0
- package/dist/specs.d.ts.map +1 -0
- package/dist/specs.js +231 -0
- package/dist/specs.js.map +1 -0
- package/dist/storage.d.ts +77 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +124 -0
- package/dist/storage.js.map +1 -0
- package/dist/styles.d.ts +29 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +876 -0
- package/dist/styles.js.map +1 -0
- package/dist/ui-control.d.ts +8 -0
- package/dist/ui-control.d.ts.map +1 -0
- package/dist/ui-control.js +8 -0
- package/dist/ui-control.js.map +1 -0
- package/dist/ui-render.d.ts +75 -0
- package/dist/ui-render.d.ts.map +1 -0
- package/dist/ui-render.js +617 -0
- package/dist/ui-render.js.map +1 -0
- package/dist/ui-spec.d.ts +143 -0
- package/dist/ui-spec.d.ts.map +1 -0
- package/dist/ui-spec.js +288 -0
- package/dist/ui-spec.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vedat Aydın Uğur
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# sgiant-ai-widget
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, embeddable **AI chat widget**. One call mounts a floating
|
|
4
|
+
bubble and a streaming chat panel into any page.
|
|
5
|
+
|
|
6
|
+
No React. No framework. Vanilla DOM, themeable through CSS custom properties,
|
|
7
|
+
and one runtime dependency.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install sgiant-ai-widget
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**[Live example →](https://vedatugur.github.io/sgiant-ai-widget/)** — a plain
|
|
14
|
+
HTML page with no bundler and no build step.
|
|
15
|
+
|
|
16
|
+
## Two ways in
|
|
17
|
+
|
|
18
|
+
### As a module (React, Next, Vue, Svelte, plain ESM)
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createAiChatWidget } from "sgiant-ai-widget";
|
|
22
|
+
|
|
23
|
+
const chat = createAiChatWidget({
|
|
24
|
+
endpoint: "https://api.example.com/chat",
|
|
25
|
+
title: "Aria",
|
|
26
|
+
greeting: "Hi! Ask me anything.",
|
|
27
|
+
accent: "#6d28d9",
|
|
28
|
+
});
|
|
29
|
+
chat.open();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### As a `<script>` tag, no build step
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<script src="https://unpkg.com/sgiant-ai-widget/dist/sgiant-ai-widget.global.js"></script>
|
|
36
|
+
<script>
|
|
37
|
+
SgiantAiWidget.createAiChatWidget({ endpoint: "/chat" });
|
|
38
|
+
</script>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The global build bundles its one dependency, so it is a single file. If your
|
|
42
|
+
page already uses a bundler, prefer the module build — it shares that dependency
|
|
43
|
+
instead of carrying a second copy.
|
|
44
|
+
|
|
45
|
+
## The backend
|
|
46
|
+
|
|
47
|
+
One POST endpoint that streams newline-delimited JSON. Four frame types matter:
|
|
48
|
+
|
|
49
|
+
| frame | meaning |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `{"type":"text","d":"…"}` | append this text |
|
|
52
|
+
| `{"threadId":"…"}` | name the conversation |
|
|
53
|
+
| `{"type":"done"}` | the turn is over |
|
|
54
|
+
| `{"type":"error","message":"…"}` | the turn failed |
|
|
55
|
+
|
|
56
|
+
The full contract, including a dependency-free reference server short enough to
|
|
57
|
+
read in one go, is in **[BACKEND.md](./BACKEND.md)**.
|
|
58
|
+
|
|
59
|
+
## Theming
|
|
60
|
+
|
|
61
|
+
The entire stylesheet reads from `--aiw-*` CSS custom properties set on the
|
|
62
|
+
widget's roots. `accent` / `gradient` are the shorthand; the `theme` option
|
|
63
|
+
overrides any token — or theme from plain CSS by targeting the roots.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
createAiChatWidget({
|
|
67
|
+
// …
|
|
68
|
+
accent: "#6d28d9",
|
|
69
|
+
theme: {
|
|
70
|
+
// dark mode
|
|
71
|
+
surface: "#16161e",
|
|
72
|
+
"surface-2": "#22222c",
|
|
73
|
+
bg: "#101016",
|
|
74
|
+
text: "#e7e7ec",
|
|
75
|
+
"text-2": "#b6b6c2",
|
|
76
|
+
muted: "#8a8a96",
|
|
77
|
+
border: "#2b2b36",
|
|
78
|
+
"border-strong": "#3a3a48",
|
|
79
|
+
"border-soft": "#22222c",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Tokens: `accent`, `accent-contrast`, `gradient`, `surface`, `surface-2`, `bg`,
|
|
85
|
+
`text`, `text-2`, `muted`, `border`, `border-strong`, `border-soft`.
|
|
86
|
+
|
|
87
|
+
## Custom renderers (extension point)
|
|
88
|
+
|
|
89
|
+
The assistant can emit `[[<tag>:{json}]]` directives; built-ins cover
|
|
90
|
+
`widget` (stat/kpis/list/table), `preview` (sandboxed HTML), `navigate`,
|
|
91
|
+
`action`, `chips`, and `form`. Register your own tags to render anything else —
|
|
92
|
+
the widget strips the directive, mounts a host element in the log, and hands
|
|
93
|
+
you the parsed JSON:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const chat = createAiChatWidget({
|
|
97
|
+
// …
|
|
98
|
+
renderers: {
|
|
99
|
+
calendar: (host, spec) => {
|
|
100
|
+
host.textContent = renderMyCalendar(spec); // validate spec yourself
|
|
101
|
+
return () => cleanup(); // optional disposer (message cleared)
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
// or later:
|
|
106
|
+
chat.registerRenderer("map", (host, spec) => drawMap(host, spec));
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Built-in tags are reserved (a plugin can't hijack the confirm-gated
|
|
110
|
+
`action`/`form` security model). A throwing renderer removes its host card
|
|
111
|
+
instead of breaking the message.
|
|
112
|
+
|
|
113
|
+
## Charts without React
|
|
114
|
+
|
|
115
|
+
In-app hosts wire `renderDataWidget` (real `@sgiant/ui` charts). A standalone
|
|
116
|
+
embed has no React, so `render_chart` frames degrade to a stat/table — unless
|
|
117
|
+
you plug `renderChartFallback(host, spec, rows, comparisonRows)` with any
|
|
118
|
+
lightweight chart lib.
|
|
119
|
+
|
|
120
|
+
## Transport
|
|
121
|
+
|
|
122
|
+
POSTs `{ accountId, threadId, content }` and reads a streamed body, tolerating
|
|
123
|
+
both shapes the platform emits:
|
|
124
|
+
|
|
125
|
+
- SSE — `data: {"type":"assistant_delta","text":"…"}`
|
|
126
|
+
- NDJSON — `{"d":"…"}`
|
|
127
|
+
|
|
128
|
+
plus `{type:"thread",threadId}`, `{type:"error",message}`, `{type:"done"}`.
|
|
129
|
+
|
|
130
|
+
## Security
|
|
131
|
+
|
|
132
|
+
The widget holds no long-lived secret — the host supplies a token (Clerk session
|
|
133
|
+
in-app, or a short-lived **embed token** for external sites). The backend's OBO
|
|
134
|
+
scoping is the real ceiling; this is presentation only. The public embed-token
|
|
135
|
+
endpoint is the next backend step (tracked with the managed-AI build).
|
|
136
|
+
|
|
137
|
+
## External embedding
|
|
138
|
+
|
|
139
|
+
For drop-in `<script>` use on third-party sites, bundle this module to a single
|
|
140
|
+
IIFE/UMD file that exposes `window.SgiantChat.init(opts)` — not yet wired (needs
|
|
141
|
+
the embed-token endpoint first).
|
|
142
|
+
|
|
143
|
+
## About the `#123` references in the source
|
|
144
|
+
|
|
145
|
+
The comments cite issue numbers from the private tracker this widget was built
|
|
146
|
+
in, where it lived inside a monorepo until 2026-09-02. They are kept because the
|
|
147
|
+
reasoning around them is worth more than the tidiness of removing it — a comment
|
|
148
|
+
explaining *why* a wire value must not change is useful even when you cannot
|
|
149
|
+
open the ticket it cites.
|
|
150
|
+
|
|
151
|
+
Nothing behind those numbers is needed to use, read, or modify this package.
|
|
152
|
+
|
|
153
|
+
## Licence
|
|
154
|
+
|
|
155
|
+
MIT © Vedat Aydın Uğur
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cross-tab "the assistant changed something" bus.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately knows NOTHING about what changed: it carries an account id and a
|
|
5
|
+
* list of opaque domain strings, and the host decides what they mean. sgiant's
|
|
6
|
+
* mapping from those strings to query keys lives in `@sgiant/ai-apply`, because
|
|
7
|
+
* it is a description of sgiant's product rather than of this widget.
|
|
8
|
+
*/
|
|
9
|
+
export interface AiChangeEvent {
|
|
10
|
+
accountId: string;
|
|
11
|
+
domains?: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Tell other tabs an AI change landed so they invalidate too. No-op where
|
|
14
|
+
* BroadcastChannel is unavailable (old browsers / SSR). */
|
|
15
|
+
export declare function broadcastAiChange(accountId: string, domains?: string[]): void;
|
|
16
|
+
/** Subscribe to cross-tab AI change events; returns an unsubscribe fn. */
|
|
17
|
+
export declare function subscribeAiChange(cb: (e: AiChangeEvent) => void): () => void;
|
|
18
|
+
export interface LiveSyncOptions {
|
|
19
|
+
/** The SSE endpoint, e.g. `${API_BASE}/accounts/:id/live`. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** Fresh auth token per (re)connect. */
|
|
22
|
+
getToken: () => Promise<string | null>;
|
|
23
|
+
/** Called for each change event the server pushes. */
|
|
24
|
+
onChange: (accountId: string, domains?: string[]) => void;
|
|
25
|
+
}
|
|
26
|
+
/** Subscribe to the server's live-sync stream. Returns an unsubscribe fn. No-op
|
|
27
|
+
* where fetch/streams are unavailable (SSR). */
|
|
28
|
+
export declare function subscribeLiveSync(opts: LiveSyncOptions): () => void;
|
|
29
|
+
//# sourceMappingURL=ai-invalidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-invalidation.d.ts","sourceRoot":"","sources":["../src/ai-invalidation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;4DAC4D;AAC5D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAsB7E;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAgB5E;AAQD,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,sDAAsD;IACtD,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC3D;AAED;iDACiD;AACjD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,IAAI,CA0DnE"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cross-tab "the assistant changed something" bus.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately knows NOTHING about what changed: it carries an account id and a
|
|
5
|
+
* list of opaque domain strings, and the host decides what they mean. sgiant's
|
|
6
|
+
* mapping from those strings to query keys lives in `@sgiant/ai-apply`, because
|
|
7
|
+
* it is a description of sgiant's product rather than of this widget.
|
|
8
|
+
*/
|
|
9
|
+
// Neutral, like the bridge's channel: this ships to people whose product is
|
|
10
|
+
// not sgiant. It is a WIRE value between tabs of one deployment, so a change
|
|
11
|
+
// only costs an old tab and a new tab not seeing each other until reload.
|
|
12
|
+
const AI_LIVE_CHANNEL = "ai-widget-live";
|
|
13
|
+
/** Tell other tabs an AI change landed so they invalidate too. No-op where
|
|
14
|
+
* BroadcastChannel is unavailable (old browsers / SSR). */
|
|
15
|
+
export function broadcastAiChange(accountId, domains) {
|
|
16
|
+
// SAME-TAB listeners (e.g. the brand info bar's live "updated by AI" status)
|
|
17
|
+
// get a synchronous window event — BroadcastChannel is meant for OTHER tabs and
|
|
18
|
+
// is the less reliable path within one document, so this is the primary signal
|
|
19
|
+
// for anything on the current page; BroadcastChannel below still covers tabs.
|
|
20
|
+
if (typeof window !== "undefined") {
|
|
21
|
+
try {
|
|
22
|
+
window.dispatchEvent(new CustomEvent(AI_LIVE_CHANNEL, { detail: { accountId, domains } }));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* CustomEvent unavailable */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (typeof BroadcastChannel === "undefined")
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
const ch = new BroadcastChannel(AI_LIVE_CHANNEL);
|
|
32
|
+
ch.postMessage({ accountId, domains });
|
|
33
|
+
ch.close();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* channel unavailable */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Subscribe to cross-tab AI change events; returns an unsubscribe fn. */
|
|
40
|
+
export function subscribeAiChange(cb) {
|
|
41
|
+
if (typeof BroadcastChannel === "undefined")
|
|
42
|
+
return () => { };
|
|
43
|
+
const ch = new BroadcastChannel(AI_LIVE_CHANNEL);
|
|
44
|
+
const handler = (ev) => {
|
|
45
|
+
const d = ev.data;
|
|
46
|
+
if (d && typeof d.accountId === "string")
|
|
47
|
+
cb({
|
|
48
|
+
accountId: d.accountId,
|
|
49
|
+
domains: Array.isArray(d.domains) ? d.domains : undefined,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
ch.addEventListener("message", handler);
|
|
53
|
+
return () => {
|
|
54
|
+
ch.removeEventListener("message", handler);
|
|
55
|
+
ch.close();
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Subscribe to the server's live-sync stream. Returns an unsubscribe fn. No-op
|
|
59
|
+
* where fetch/streams are unavailable (SSR). */
|
|
60
|
+
export function subscribeLiveSync(opts) {
|
|
61
|
+
if (typeof fetch === "undefined")
|
|
62
|
+
return () => { };
|
|
63
|
+
let closed = false;
|
|
64
|
+
let ctrl = null;
|
|
65
|
+
let backoff = 1000;
|
|
66
|
+
const connect = async () => {
|
|
67
|
+
if (closed)
|
|
68
|
+
return;
|
|
69
|
+
ctrl = new AbortController();
|
|
70
|
+
try {
|
|
71
|
+
const token = await opts.getToken();
|
|
72
|
+
const res = await fetch(opts.url, {
|
|
73
|
+
headers: token ? { authorization: `Bearer ${token}` } : {},
|
|
74
|
+
credentials: "include",
|
|
75
|
+
signal: ctrl.signal,
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok || !res.body)
|
|
78
|
+
throw new Error(`live ${res.status}`);
|
|
79
|
+
backoff = 1000; // connected — reset backoff
|
|
80
|
+
const reader = res.body.getReader();
|
|
81
|
+
const dec = new TextDecoder();
|
|
82
|
+
let buf = "";
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { value, done } = await reader.read();
|
|
85
|
+
if (done || closed)
|
|
86
|
+
break;
|
|
87
|
+
buf += dec.decode(value, { stream: true });
|
|
88
|
+
let nl;
|
|
89
|
+
while ((nl = buf.indexOf("\n\n")) >= 0) {
|
|
90
|
+
const frame = buf.slice(0, nl);
|
|
91
|
+
buf = buf.slice(nl + 2);
|
|
92
|
+
const line = frame.split("\n").find((l) => l.startsWith("data:"));
|
|
93
|
+
if (!line)
|
|
94
|
+
continue; // heartbeat / comment
|
|
95
|
+
try {
|
|
96
|
+
const d = JSON.parse(line.slice(5).trim());
|
|
97
|
+
if (d && typeof d.accountId === "string")
|
|
98
|
+
opts.onChange(d.accountId, Array.isArray(d.domains) ? d.domains : undefined);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* non-json frame */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* network error / abort */
|
|
108
|
+
}
|
|
109
|
+
if (closed)
|
|
110
|
+
return;
|
|
111
|
+
const wait = backoff;
|
|
112
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
113
|
+
setTimeout(connect, wait);
|
|
114
|
+
};
|
|
115
|
+
void connect();
|
|
116
|
+
return () => {
|
|
117
|
+
closed = true;
|
|
118
|
+
ctrl?.abort();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=ai-invalidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-invalidation.js","sourceRoot":"","sources":["../src/ai-invalidation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accent/foreground contrast, WCAG 2.1 relative luminance (#307).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the widget shipped `--aiw-accent-contrast: #fff`, correct
|
|
5
|
+
* against its own violet default (7.10:1) and never overridden by a single
|
|
6
|
+
* host. All three of ours pass a brand accent and none passes a contrast
|
|
7
|
+
* colour, so every accent-filled control rendered white on teal `#60C7C8` —
|
|
8
|
+
* **2.00:1**, against a 4.5:1 floor. Seventeen declarations, thirteen controls,
|
|
9
|
+
* four surfaces.
|
|
10
|
+
*
|
|
11
|
+
* A default cannot fix that, because the failing value IS the default. The
|
|
12
|
+
* pairing has to be DERIVED from whatever accent arrives — which also makes the
|
|
13
|
+
* widget safe to publish (#306), where we will not control the accent at all.
|
|
14
|
+
*
|
|
15
|
+
* No dependency on @sgiant/tokens on purpose: this file has to survive being
|
|
16
|
+
* extracted into a public package.
|
|
17
|
+
*/
|
|
18
|
+
/** The two foregrounds we choose between. Both are brand values, and both are
|
|
19
|
+
* near enough to black/white to behave sensibly for an arbitrary accent a
|
|
20
|
+
* third-party host passes. */
|
|
21
|
+
export declare const CONTRAST_DARK = "#151D2F";
|
|
22
|
+
export declare const CONTRAST_LIGHT = "#FCF7E3";
|
|
23
|
+
/** WCAG 2.1 relative luminance. */
|
|
24
|
+
export declare function relativeLuminance(hex: string): number | null;
|
|
25
|
+
/** WCAG contrast ratio, 1–21. Null when either colour is unparseable. */
|
|
26
|
+
export declare function contrastRatio(a: string, b: string): number | null;
|
|
27
|
+
/**
|
|
28
|
+
* The foreground to use on a given accent: whichever of dark/light contrasts
|
|
29
|
+
* more. Returns null for anything it cannot parse — `color-mix()`, a CSS
|
|
30
|
+
* variable, `rebeccapurple` — so the caller leaves the host's own value alone
|
|
31
|
+
* instead of overwriting it with a guess.
|
|
32
|
+
*
|
|
33
|
+
* Measured on the accents this repo actually ships: teal → navy 8.41, amber →
|
|
34
|
+
* navy 8.73, orange → navy 5.98, and the violet default → cream 6.62. All four
|
|
35
|
+
* clear 4.5:1, which the host repo's contrast test asserts, so the
|
|
36
|
+
* next brand-colour change cannot quietly undo it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveAccentContrast(accent: string): string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Mix two hex colours in sRGB, the same way CSS `color-mix(in srgb, …)` does.
|
|
41
|
+
*
|
|
42
|
+
* Exists because the user's message bubble is not painted with the accent — it
|
|
43
|
+
* is painted with the accent mixed 76% into a near-black navy, and a foreground
|
|
44
|
+
* derived from the RAW accent is derived against the wrong background. Over the
|
|
45
|
+
* teal all three hosts pass, that bubble resolves to `#4a9d9e` and the sheet's
|
|
46
|
+
* literal `#fff` measures **3.18:1** on it: #307's defect, still live in the
|
|
47
|
+
* most-used element in the widget, because the fix was applied to the token and
|
|
48
|
+
* the bubble never read the token.
|
|
49
|
+
*
|
|
50
|
+
* The mix is computed HERE and handed to CSS as a resolved value, rather than
|
|
51
|
+
* computed here AND written as a `color-mix()` next door. Two implementations of
|
|
52
|
+
* one blend is how the foreground and the background drift apart, which is the
|
|
53
|
+
* whole bug again one level down.
|
|
54
|
+
*
|
|
55
|
+
* Null when either colour is unparseable — the caller must then leave whatever
|
|
56
|
+
* was there alone rather than guess, exactly as `resolveAccentContrast` does.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mixSrgb(a: string, b: string, ratioA: number): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* The accent, adjusted until it is legible as TEXT on a given surface.
|
|
61
|
+
*
|
|
62
|
+
* `--aiw-accent` is a FILL colour. Twenty rules also use it as ink — links,
|
|
63
|
+
* hover states, the focused input, chips, KPI deltas, the menu icons, the
|
|
64
|
+
* active vote — and a fill is not an ink. Measured against the surfaces this
|
|
65
|
+
* sheet actually paints:
|
|
66
|
+
*
|
|
67
|
+
* teal #60C7C8 (what all three hosts pass) 2.00:1 on white, 9.05 on dark
|
|
68
|
+
* violet #6d28d9 (the widget's own default) 7.10:1 on white, 2.55 on dark
|
|
69
|
+
* amber #FBAA34 1.93:1 on white, 9.39 on dark
|
|
70
|
+
*
|
|
71
|
+
* So it is not "the teal is a bad accent". Every accent is illegible as ink on
|
|
72
|
+
* one of the two schemes; the widget's own default is the one that fails in the
|
|
73
|
+
* dark. Which scheme breaks depends only on where the accent sits between the
|
|
74
|
+
* two surfaces, and that is a property of the colour, not a mistake in it.
|
|
75
|
+
*
|
|
76
|
+
* The adjustment blends toward black or white — whichever the surface is
|
|
77
|
+
* further from — and returns the FIRST step that clears the floor, so the ink
|
|
78
|
+
* stays as close to the brand accent as legibility allows rather than jumping
|
|
79
|
+
* to a safe navy. Hue survives; some chroma does not.
|
|
80
|
+
*
|
|
81
|
+
* 5% steps because that is finer than the eye reads as a colour change and
|
|
82
|
+
* still terminates in twenty iterations. Null when the accent is unparseable —
|
|
83
|
+
* the caller leaves the token alone and the sheet's own default applies, which
|
|
84
|
+
* is the same contract as `resolveAccentContrast`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function accentInk(accent: string, surface: string, target?: number): string | null;
|
|
87
|
+
//# sourceMappingURL=contrast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;+BAE+B;AAC/B,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,cAAc,YAAY,CAAC;AAqBxC,mCAAmC;AACnC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU3E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,SAAM,GACX,MAAM,GAAG,IAAI,CAWf"}
|
package/dist/contrast.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accent/foreground contrast, WCAG 2.1 relative luminance (#307).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: the widget shipped `--aiw-accent-contrast: #fff`, correct
|
|
5
|
+
* against its own violet default (7.10:1) and never overridden by a single
|
|
6
|
+
* host. All three of ours pass a brand accent and none passes a contrast
|
|
7
|
+
* colour, so every accent-filled control rendered white on teal `#60C7C8` —
|
|
8
|
+
* **2.00:1**, against a 4.5:1 floor. Seventeen declarations, thirteen controls,
|
|
9
|
+
* four surfaces.
|
|
10
|
+
*
|
|
11
|
+
* A default cannot fix that, because the failing value IS the default. The
|
|
12
|
+
* pairing has to be DERIVED from whatever accent arrives — which also makes the
|
|
13
|
+
* widget safe to publish (#306), where we will not control the accent at all.
|
|
14
|
+
*
|
|
15
|
+
* No dependency on @sgiant/tokens on purpose: this file has to survive being
|
|
16
|
+
* extracted into a public package.
|
|
17
|
+
*/
|
|
18
|
+
/** The two foregrounds we choose between. Both are brand values, and both are
|
|
19
|
+
* near enough to black/white to behave sensibly for an arbitrary accent a
|
|
20
|
+
* third-party host passes. */
|
|
21
|
+
export const CONTRAST_DARK = "#151D2F"; // brand navy
|
|
22
|
+
export const CONTRAST_LIGHT = "#FCF7E3"; // brand cream
|
|
23
|
+
/** #rgb, #rrggbb, or #rrggbbaa → [r,g,b] 0-255. Null when unparseable — a
|
|
24
|
+
* caller must then keep whatever it already had rather than guess. */
|
|
25
|
+
function parseHex(hex) {
|
|
26
|
+
const h = hex.trim().replace(/^#/, "");
|
|
27
|
+
if (h.length === 3) {
|
|
28
|
+
const [r, g, b] = h.split("");
|
|
29
|
+
if (!/^[0-9a-f]{3}$/i.test(h))
|
|
30
|
+
return null;
|
|
31
|
+
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];
|
|
32
|
+
}
|
|
33
|
+
if ((h.length === 6 || h.length === 8) && /^[0-9a-f]+$/i.test(h)) {
|
|
34
|
+
return [
|
|
35
|
+
parseInt(h.slice(0, 2), 16),
|
|
36
|
+
parseInt(h.slice(2, 4), 16),
|
|
37
|
+
parseInt(h.slice(4, 6), 16),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/** WCAG 2.1 relative luminance. */
|
|
43
|
+
export function relativeLuminance(hex) {
|
|
44
|
+
const rgb = parseHex(hex);
|
|
45
|
+
if (!rgb)
|
|
46
|
+
return null;
|
|
47
|
+
const [r, g, b] = rgb.map((c) => {
|
|
48
|
+
const s = c / 255;
|
|
49
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
50
|
+
});
|
|
51
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
52
|
+
}
|
|
53
|
+
/** WCAG contrast ratio, 1–21. Null when either colour is unparseable. */
|
|
54
|
+
export function contrastRatio(a, b) {
|
|
55
|
+
const la = relativeLuminance(a);
|
|
56
|
+
const lb = relativeLuminance(b);
|
|
57
|
+
if (la === null || lb === null)
|
|
58
|
+
return null;
|
|
59
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The foreground to use on a given accent: whichever of dark/light contrasts
|
|
63
|
+
* more. Returns null for anything it cannot parse — `color-mix()`, a CSS
|
|
64
|
+
* variable, `rebeccapurple` — so the caller leaves the host's own value alone
|
|
65
|
+
* instead of overwriting it with a guess.
|
|
66
|
+
*
|
|
67
|
+
* Measured on the accents this repo actually ships: teal → navy 8.41, amber →
|
|
68
|
+
* navy 8.73, orange → navy 5.98, and the violet default → cream 6.62. All four
|
|
69
|
+
* clear 4.5:1, which the host repo's contrast test asserts, so the
|
|
70
|
+
* next brand-colour change cannot quietly undo it.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveAccentContrast(accent) {
|
|
73
|
+
const dark = contrastRatio(accent, CONTRAST_DARK);
|
|
74
|
+
const light = contrastRatio(accent, CONTRAST_LIGHT);
|
|
75
|
+
if (dark === null || light === null)
|
|
76
|
+
return null;
|
|
77
|
+
return dark >= light ? CONTRAST_DARK : CONTRAST_LIGHT;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Mix two hex colours in sRGB, the same way CSS `color-mix(in srgb, …)` does.
|
|
81
|
+
*
|
|
82
|
+
* Exists because the user's message bubble is not painted with the accent — it
|
|
83
|
+
* is painted with the accent mixed 76% into a near-black navy, and a foreground
|
|
84
|
+
* derived from the RAW accent is derived against the wrong background. Over the
|
|
85
|
+
* teal all three hosts pass, that bubble resolves to `#4a9d9e` and the sheet's
|
|
86
|
+
* literal `#fff` measures **3.18:1** on it: #307's defect, still live in the
|
|
87
|
+
* most-used element in the widget, because the fix was applied to the token and
|
|
88
|
+
* the bubble never read the token.
|
|
89
|
+
*
|
|
90
|
+
* The mix is computed HERE and handed to CSS as a resolved value, rather than
|
|
91
|
+
* computed here AND written as a `color-mix()` next door. Two implementations of
|
|
92
|
+
* one blend is how the foreground and the background drift apart, which is the
|
|
93
|
+
* whole bug again one level down.
|
|
94
|
+
*
|
|
95
|
+
* Null when either colour is unparseable — the caller must then leave whatever
|
|
96
|
+
* was there alone rather than guess, exactly as `resolveAccentContrast` does.
|
|
97
|
+
*/
|
|
98
|
+
export function mixSrgb(a, b, ratioA) {
|
|
99
|
+
const pa = parseHex(a);
|
|
100
|
+
const pb = parseHex(b);
|
|
101
|
+
if (!pa || !pb)
|
|
102
|
+
return null;
|
|
103
|
+
const p = Math.min(1, Math.max(0, ratioA));
|
|
104
|
+
const chan = (i) => Math.round(pa[i] * p + pb[i] * (1 - p))
|
|
105
|
+
.toString(16)
|
|
106
|
+
.padStart(2, "0");
|
|
107
|
+
return `#${chan(0)}${chan(1)}${chan(2)}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The accent, adjusted until it is legible as TEXT on a given surface.
|
|
111
|
+
*
|
|
112
|
+
* `--aiw-accent` is a FILL colour. Twenty rules also use it as ink — links,
|
|
113
|
+
* hover states, the focused input, chips, KPI deltas, the menu icons, the
|
|
114
|
+
* active vote — and a fill is not an ink. Measured against the surfaces this
|
|
115
|
+
* sheet actually paints:
|
|
116
|
+
*
|
|
117
|
+
* teal #60C7C8 (what all three hosts pass) 2.00:1 on white, 9.05 on dark
|
|
118
|
+
* violet #6d28d9 (the widget's own default) 7.10:1 on white, 2.55 on dark
|
|
119
|
+
* amber #FBAA34 1.93:1 on white, 9.39 on dark
|
|
120
|
+
*
|
|
121
|
+
* So it is not "the teal is a bad accent". Every accent is illegible as ink on
|
|
122
|
+
* one of the two schemes; the widget's own default is the one that fails in the
|
|
123
|
+
* dark. Which scheme breaks depends only on where the accent sits between the
|
|
124
|
+
* two surfaces, and that is a property of the colour, not a mistake in it.
|
|
125
|
+
*
|
|
126
|
+
* The adjustment blends toward black or white — whichever the surface is
|
|
127
|
+
* further from — and returns the FIRST step that clears the floor, so the ink
|
|
128
|
+
* stays as close to the brand accent as legibility allows rather than jumping
|
|
129
|
+
* to a safe navy. Hue survives; some chroma does not.
|
|
130
|
+
*
|
|
131
|
+
* 5% steps because that is finer than the eye reads as a colour change and
|
|
132
|
+
* still terminates in twenty iterations. Null when the accent is unparseable —
|
|
133
|
+
* the caller leaves the token alone and the sheet's own default applies, which
|
|
134
|
+
* is the same contract as `resolveAccentContrast`.
|
|
135
|
+
*/
|
|
136
|
+
export function accentInk(accent, surface, target = 4.5) {
|
|
137
|
+
const surfaceLum = relativeLuminance(surface);
|
|
138
|
+
if (surfaceLum === null || parseHex(accent) === null)
|
|
139
|
+
return null;
|
|
140
|
+
const pole = surfaceLum > 0.5 ? "#000000" : "#ffffff";
|
|
141
|
+
for (let p = 1; p >= 0; p -= 0.05) {
|
|
142
|
+
const candidate = mixSrgb(accent, pole, p);
|
|
143
|
+
if (!candidate)
|
|
144
|
+
return null;
|
|
145
|
+
const ratio = contrastRatio(candidate, surface);
|
|
146
|
+
if (ratio !== null && ratio >= target)
|
|
147
|
+
return candidate;
|
|
148
|
+
}
|
|
149
|
+
return pole;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=contrast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `[[tag:{json}]]` directive extractor the chat widget runs over every
|
|
3
|
+
* assistant reply.
|
|
4
|
+
*
|
|
5
|
+
* Lives in its own module because it is pure and because getting it wrong is
|
|
6
|
+
* VISIBLE: whatever this function fails to consume is shown to the user as raw
|
|
7
|
+
* text in the middle of a sentence. That makes it worth pinning down with
|
|
8
|
+
* tests, which the widget itself (imperative DOM, no harness) cannot be.
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedDirective<T> {
|
|
11
|
+
spec: T;
|
|
12
|
+
/** The message with this directive removed. */
|
|
13
|
+
stripped: string;
|
|
14
|
+
start: number;
|
|
15
|
+
/** Index just past everything consumed — the directive leaves no residue. */
|
|
16
|
+
end: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Pull the FIRST `[[tag:{json}]]` out of `text`.
|
|
20
|
+
*
|
|
21
|
+
* Forgiving by design, because the delimiters are the part a model gets wrong
|
|
22
|
+
* most often and every slip is shown to the user as raw punctuation:
|
|
23
|
+
*
|
|
24
|
+
* - the JSON object is BRACE-MATCHED, string- and escape-aware, so a `}` or
|
|
25
|
+
* `]]` inside a quoted caption cannot end it early;
|
|
26
|
+
* - 0–2 trailing `]` are consumed, so `…}]]`, `…}]` and `…}` all parse;
|
|
27
|
+
* - a surplus `}` before the brackets is consumed too. A model that closes one
|
|
28
|
+
* brace too many is common, and it used to leave `}]]` sitting in the reply:
|
|
29
|
+
* the matcher stopped at the REAL closing brace (correctly), then looked for
|
|
30
|
+
* `]`, found `}`, and gave up — so the card rendered perfectly and the user
|
|
31
|
+
* still saw `}]]` printed above it.
|
|
32
|
+
*
|
|
33
|
+
* Only ADJACENT residue is swallowed. Once a character is neither `}` nor `]`,
|
|
34
|
+
* consumption stops, so nothing the assistant actually wrote is eaten.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseJsonDirective<T>(text: string, tag: string): ParsedDirective<T> | null;
|
|
37
|
+
//# sourceMappingURL=directive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directive.d.ts","sourceRoot":"","sources":["../src/directive.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,CA8C3B"}
|