@alexkroman1/aai-ui 0.9.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_components/app.d.ts +5 -0
  3. package/dist/_components/app.js +23 -0
  4. package/dist/_components/button.d.ts +11 -0
  5. package/dist/_components/button.js +22 -0
  6. package/dist/_components/chat-view.d.ts +5 -0
  7. package/dist/_components/chat-view.js +37 -0
  8. package/dist/_components/controls.d.ts +4 -0
  9. package/dist/_components/controls.js +23 -0
  10. package/dist/_components/error-banner.d.ts +7 -0
  11. package/dist/_components/error-banner.js +13 -0
  12. package/dist/_components/message-bubble.d.ts +7 -0
  13. package/dist/_components/message-bubble.js +19 -0
  14. package/dist/_components/message-list.d.ts +4 -0
  15. package/dist/_components/message-list.js +59 -0
  16. package/dist/_components/sidebar-layout.d.ts +21 -0
  17. package/dist/_components/sidebar-layout.js +36 -0
  18. package/dist/_components/start-screen.d.ts +26 -0
  19. package/dist/_components/start-screen.js +51 -0
  20. package/dist/_components/state-indicator.d.ts +7 -0
  21. package/dist/_components/state-indicator.js +15 -0
  22. package/dist/_components/thinking-indicator.d.ts +5 -0
  23. package/dist/_components/thinking-indicator.js +22 -0
  24. package/dist/_components/tool-call-block.d.ts +7 -0
  25. package/dist/_components/tool-call-block.js +96 -0
  26. package/dist/_components/tool-icons.d.ts +17 -0
  27. package/dist/_components/tool-icons.js +128 -0
  28. package/dist/_components/transcript.d.ts +7 -0
  29. package/dist/_components/transcript.js +17 -0
  30. package/dist/_jsdom-setup.d.ts +0 -0
  31. package/dist/audio.d.ts +45 -0
  32. package/dist/audio.js +111 -0
  33. package/dist/components.d.ts +31 -0
  34. package/dist/components.js +17 -0
  35. package/dist/index.d.ts +20 -0
  36. package/dist/index.js +19 -0
  37. package/dist/mount-context.d.ts +33 -0
  38. package/dist/mount-context.js +15 -0
  39. package/dist/mount.d.ts +51 -0
  40. package/dist/mount.js +70 -0
  41. package/dist/session.d.ts +100 -0
  42. package/dist/session.js +359 -0
  43. package/dist/signals.d.ts +89 -0
  44. package/dist/signals.js +138 -0
  45. package/dist/tsdown.config.d.ts +2 -0
  46. package/dist/types.d.ts +75 -0
  47. package/dist/types.js +5 -0
  48. package/dist/worklets/capture-processor.d.ts +2 -0
  49. package/dist/worklets/capture-processor.js +57 -0
  50. package/dist/worklets/playback-processor.d.ts +2 -0
  51. package/dist/worklets/playback-processor.js +105 -0
  52. package/package.json +56 -0
  53. package/styles.css +74 -0
@@ -0,0 +1,57 @@
1
+ //#region worklets/capture-processor.ts
2
+ const script = new Blob([`
3
+ class CaptureProcessor extends AudioWorkletProcessor {
4
+ constructor(options) {
5
+ super();
6
+ this.recording = false;
7
+ const opts = options.processorOptions || {};
8
+ this.fromRate = opts.contextRate || sampleRate;
9
+ this.toRate = opts.sttSampleRate || sampleRate;
10
+ this.ratio = this.fromRate / this.toRate;
11
+ this.needsResample = this.fromRate !== this.toRate;
12
+ this.port.onmessage = (e) => {
13
+ if (e.data.event === 'start') this.recording = true;
14
+ else if (e.data.event === 'stop') this.recording = false;
15
+ };
16
+ }
17
+
18
+ resample(input) {
19
+ const ratio = this.ratio;
20
+ const outLen = Math.ceil(input.length / ratio);
21
+ const out = new Float32Array(outLen);
22
+ for (let i = 0; i < outLen; i++) {
23
+ const srcIdx = i * ratio;
24
+ const idx = srcIdx | 0;
25
+ const frac = srcIdx - idx;
26
+ const a = input[idx];
27
+ const b = idx + 1 < input.length ? input[idx + 1] : a;
28
+ out[i] = a + frac * (b - a);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ process(inputs) {
34
+ const input = inputs[0];
35
+ if (!input || !input[0] || !this.recording) return true;
36
+
37
+ const raw = input[0];
38
+ const samples = this.needsResample ? this.resample(raw) : raw;
39
+
40
+ // Convert Float32 -> Int16
41
+ const buffer = new ArrayBuffer(samples.length * 2);
42
+ const view = new DataView(buffer);
43
+ for (let i = 0; i < samples.length; i++) {
44
+ const s = Math.max(-1, Math.min(1, samples[i]));
45
+ view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
46
+ }
47
+
48
+ this.port.postMessage({ event: 'chunk', buffer }, [buffer]);
49
+ return true;
50
+ }
51
+ }
52
+
53
+ registerProcessor('capture-processor', CaptureProcessor);
54
+ `], { type: "application/javascript" });
55
+ const src = URL.createObjectURL(script);
56
+ //#endregion
57
+ export { src as default };
@@ -0,0 +1,2 @@
1
+ declare const src: string;
2
+ export default src;
@@ -0,0 +1,105 @@
1
+ //#region worklets/playback-processor.ts
2
+ const script = new Blob([`
3
+ class PlaybackProcessor extends AudioWorkletProcessor {
4
+ constructor(options) {
5
+ super();
6
+ this.interrupted = false;
7
+ this.isDone = false;
8
+ this.playing = false;
9
+ const rate = options.processorOptions?.sampleRate ?? 24000;
10
+ // Wait for ~400ms of audio before starting.
11
+ // If 'done' arrives first (short utterance), start immediately.
12
+ this.jitterSamples = Math.floor(rate * 0.4);
13
+ // Carry-over byte for split samples across chunks
14
+ this.carry = null;
15
+ // Float32 sample buffer — 60s at the context sample rate
16
+ this.samples = new Float32Array(rate * 60);
17
+ this.writePos = 0;
18
+ this.readPos = 0;
19
+ // Report playback position every ~50ms for word-level text sync
20
+ this.progressInterval = Math.floor(rate * 0.05);
21
+ this.samplesSinceProgress = 0;
22
+
23
+ this.port.onmessage = (e) => {
24
+ const d = e.data;
25
+ if (d.event === 'write') {
26
+ this.ingestBytes(d.buffer);
27
+ } else if (d.event === 'interrupt') {
28
+ this.interrupted = true;
29
+ } else if (d.event === 'done') {
30
+ this.isDone = true;
31
+ }
32
+ };
33
+ }
34
+
35
+ ingestBytes(uint8) {
36
+ let bytes = uint8;
37
+
38
+ if (this.carry !== null) {
39
+ const merged = new Uint8Array(1 + bytes.length);
40
+ merged[0] = this.carry;
41
+ merged.set(bytes, 1);
42
+ bytes = merged;
43
+ this.carry = null;
44
+ }
45
+
46
+ if (bytes.length % 2 !== 0) {
47
+ this.carry = bytes[bytes.length - 1];
48
+ bytes = bytes.subarray(0, bytes.length - 1);
49
+ }
50
+
51
+ if (bytes.length === 0) return;
52
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
53
+ const numSamples = bytes.length / 2;
54
+ for (let i = 0; i < numSamples; i++) {
55
+ this.samples[this.writePos++] = view.getInt16(i * 2, true) / 0x8000;
56
+ }
57
+ }
58
+
59
+ process(inputs, outputs) {
60
+ const out = outputs[0][0];
61
+ if (this.interrupted) {
62
+ this.port.postMessage({ event: 'stop' });
63
+ return false;
64
+ }
65
+
66
+ const avail = this.writePos - this.readPos;
67
+
68
+ // Wait for jitter buffer to fill, unless done (short utterance)
69
+ if (!this.playing) {
70
+ if (avail >= this.jitterSamples || this.isDone) {
71
+ this.playing = true;
72
+ } else {
73
+ out.fill(0);
74
+ return true;
75
+ }
76
+ }
77
+
78
+ if (avail > 0) {
79
+ const n = Math.min(avail, out.length);
80
+ out.set(this.samples.subarray(this.readPos, this.readPos + n));
81
+ this.readPos += n;
82
+ this.samplesSinceProgress += n;
83
+ if (this.samplesSinceProgress >= this.progressInterval) {
84
+ this.port.postMessage({ event: 'progress', readPos: this.readPos });
85
+ this.samplesSinceProgress = 0;
86
+ }
87
+ out.fill(0, n);
88
+ return true;
89
+ }
90
+
91
+ // No data: output silence, stop only when done
92
+ out.fill(0);
93
+ if (this.isDone) {
94
+ this.port.postMessage({ event: 'stop' });
95
+ return false;
96
+ }
97
+ return true;
98
+ }
99
+ }
100
+
101
+ registerProcessor('playback-processor', PlaybackProcessor);
102
+ `], { type: "application/javascript" });
103
+ const src = URL.createObjectURL(script);
104
+ //#endregion
105
+ export { src as default };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@alexkroman1/aai-ui",
3
+ "version": "0.9.0",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "styles.css"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "source": "./index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./styles.css": "./styles.css",
16
+ "./session": {
17
+ "source": "./session.ts",
18
+ "types": "./dist/session.d.ts",
19
+ "import": "./dist/session.js"
20
+ },
21
+ "./components": {
22
+ "source": "./components.ts",
23
+ "types": "./dist/components.d.ts",
24
+ "import": "./dist/components.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "clsx": "^2.1.1",
29
+ "@alexkroman1/aai": "0.9.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@preact/signals": "^2.8.2",
33
+ "preact": "^10.29.0",
34
+ "tailwindcss": "^4.2.1"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "tailwindcss": {
38
+ "optional": true
39
+ }
40
+ },
41
+ "devDependencies": {
42
+ "@preact/signals": "^2.8.2",
43
+ "@testing-library/preact": "^3.2.4",
44
+ "jsdom": "^29.0.1",
45
+ "preact": "^10.29.0",
46
+ "tsdown": "^0.21.4"
47
+ },
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown && tsc -p tsconfig.build.json",
53
+ "typecheck": "tsc --noEmit",
54
+ "lint": "biome check ."
55
+ }
56
+ }
package/styles.css ADDED
@@ -0,0 +1,74 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap");
2
+ @import "tailwindcss";
3
+ @source "./";
4
+ @source "../dist/ui/";
5
+
6
+ @theme {
7
+ --color-aai-bg: #101010;
8
+ --color-aai-surface: #151515;
9
+ --color-aai-surface-faint: rgba(255, 255, 255, 0.031);
10
+ --color-aai-surface-hover: rgba(255, 255, 255, 0.059);
11
+ --color-aai-border: #282828;
12
+ --color-aai-primary: #fab283;
13
+ --color-aai-text: rgba(255, 255, 255, 0.936);
14
+ --color-aai-text-secondary: rgba(255, 255, 255, 0.618);
15
+ --color-aai-text-muted: rgba(255, 255, 255, 0.284);
16
+ --color-aai-text-dim: rgba(255, 255, 255, 0.422);
17
+ --color-aai-error: #e06c75;
18
+ --color-aai-ring: #56b6c2;
19
+ --color-aai-state-disconnected: rgba(255, 255, 255, 0.422);
20
+ --color-aai-state-connecting: rgba(255, 255, 255, 0.422);
21
+ --color-aai-state-ready: #7fd88f;
22
+ --color-aai-state-listening: #56b6c2;
23
+ --color-aai-state-thinking: #f5a742;
24
+ --color-aai-state-speaking: #e06c75;
25
+ --color-aai-state-error: #e06c75;
26
+ --radius-aai: 6px;
27
+ --font-aai: "Inter", system-ui, -apple-system, sans-serif;
28
+ --font-aai-mono: "IBM Plex Mono", monospace;
29
+ }
30
+
31
+ @layer base {
32
+ html,
33
+ body {
34
+ margin: 0;
35
+ padding: 0;
36
+ background: var(--color-aai-bg);
37
+ }
38
+ }
39
+
40
+ @keyframes aai-bounce {
41
+ 0%,
42
+ 80%,
43
+ 100% {
44
+ opacity: 0.3;
45
+ transform: scale(0.8);
46
+ }
47
+ 40% {
48
+ opacity: 1;
49
+ transform: scale(1);
50
+ }
51
+ }
52
+
53
+ @keyframes aai-shimmer {
54
+ 0% {
55
+ background-position: -200% 0;
56
+ }
57
+ 100% {
58
+ background-position: 200% 0;
59
+ }
60
+ }
61
+
62
+ .tool-shimmer {
63
+ background: linear-gradient(
64
+ 90deg,
65
+ var(--color-aai-text) 25%,
66
+ var(--color-aai-text-dim) 50%,
67
+ var(--color-aai-text) 75%
68
+ );
69
+ background-size: 200% 100%;
70
+ -webkit-background-clip: text;
71
+ background-clip: text;
72
+ -webkit-text-fill-color: transparent;
73
+ animation: aai-shimmer 2s ease-in-out infinite;
74
+ }