@artooi/ag-ui-web-component 0.8.1 → 0.10.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 (54) hide show
  1. package/CHANGELOG.md +82 -4
  2. package/README.md +45 -7
  3. package/dist/ag-ui-web-component.bundle.js +163 -57
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/core/ag_ui_chat.d.ts +27 -1
  6. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  7. package/dist/core/agui_client.d.ts +6 -0
  8. package/dist/core/agui_client.d.ts.map +1 -1
  9. package/dist/core/attachment.d.ts +5 -0
  10. package/dist/core/attachment.d.ts.map +1 -1
  11. package/dist/core/conversation_store.d.ts +8 -0
  12. package/dist/core/conversation_store.d.ts.map +1 -1
  13. package/dist/core/remote_conversation_store.d.ts.map +1 -1
  14. package/dist/core/transcribe_audio.d.ts +25 -0
  15. package/dist/core/transcribe_audio.d.ts.map +1 -0
  16. package/dist/core/upload_attachment.d.ts +8 -2
  17. package/dist/core/upload_attachment.d.ts.map +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +735 -55
  21. package/dist/index.js.map +4 -4
  22. package/dist/ui/attachment_tray.d.ts +7 -1
  23. package/dist/ui/attachment_tray.d.ts.map +1 -1
  24. package/dist/ui/relative_time.d.ts +5 -3
  25. package/dist/ui/relative_time.d.ts.map +1 -1
  26. package/dist/ui/styles.d.ts +1 -1
  27. package/dist/ui/styles.d.ts.map +1 -1
  28. package/dist/ui/thoughts_block.d.ts +30 -0
  29. package/dist/ui/thoughts_block.d.ts.map +1 -0
  30. package/dist/ui/thread_drawer.d.ts.map +1 -1
  31. package/dist/ui/tool_call_card.d.ts.map +1 -1
  32. package/dist/ui/ui_strings.d.ts +14 -1
  33. package/dist/ui/ui_strings.d.ts.map +1 -1
  34. package/dist/ui/voice_input.d.ts +41 -0
  35. package/dist/ui/voice_input.d.ts.map +1 -0
  36. package/dist/version.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/core/ag_ui_chat.ts +217 -7
  39. package/src/core/agui_client.ts +31 -2
  40. package/src/core/attachment.ts +21 -1
  41. package/src/core/conversation_store.ts +84 -18
  42. package/src/core/remote_conversation_store.ts +24 -3
  43. package/src/core/transcribe_audio.ts +62 -0
  44. package/src/core/upload_attachment.ts +8 -1
  45. package/src/index.ts +5 -0
  46. package/src/ui/attachment_tray.ts +42 -5
  47. package/src/ui/relative_time.ts +8 -3
  48. package/src/ui/styles.ts +113 -7
  49. package/src/ui/thoughts_block.ts +83 -0
  50. package/src/ui/thread_drawer.ts +83 -9
  51. package/src/ui/tool_call_card.ts +6 -0
  52. package/src/ui/ui_strings.ts +20 -1
  53. package/src/ui/voice_input.ts +169 -0
  54. package/src/version.ts +1 -1
@@ -26,12 +26,17 @@ export interface UiStrings {
26
26
  collapse: string;
27
27
  /** Expand affordance (the sidebar rail toggle). */
28
28
  expand: string;
29
+ /** Built-in header theme toggle (light ⇄ dark). */
30
+ toggleTheme: string;
29
31
 
30
32
  // ── Messages region ─────────────────────────────────────────────────────────
31
33
  /** `aria-label` of the scrolling message log. */
32
34
  conversation: string;
33
- /** `aria-label` of the "thinking" pending indicator. */
35
+ /** `aria-label` of the "thinking" pending indicator, and the thoughts region's
36
+ * header while the model is still reasoning. */
34
37
  thinking: string;
38
+ /** The thoughts region's header once reasoning has streamed (collapsed label). */
39
+ thoughts: string;
35
40
  /** The muted note after a cancelled run. */
36
41
  stopped: string;
37
42
  /** Error shown when the stream drops without a terminal AG-UI event. */
@@ -56,6 +61,14 @@ export interface UiStrings {
56
61
  stop: string;
57
62
  /** Attach-files button. */
58
63
  attachFiles: string;
64
+ /** Mic button while idle (start recording). */
65
+ recordVoice: string;
66
+ /** Mic button while recording (stop + transcribe). */
67
+ stopRecording: string;
68
+ /** Mic button while the clip is being transcribed. */
69
+ transcribing: string;
70
+ /** Mic button fallback message when transcription fails. */
71
+ transcriptionFailed: string;
59
72
 
60
73
  // ── Tool-call card ──────────────────────────────────────────────────────────
61
74
  /** Status pill while the call runs. */
@@ -137,9 +150,11 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
137
150
  newChat: "New chat",
138
151
  collapse: "Collapse",
139
152
  expand: "Expand",
153
+ toggleTheme: "Toggle theme",
140
154
 
141
155
  conversation: "Conversation",
142
156
  thinking: "Assistant is thinking…",
157
+ thoughts: "Thoughts",
143
158
  stopped: "⏹ Stopped",
144
159
  connectionLost: "Connection lost",
145
160
  noResult: "No result returned.",
@@ -152,6 +167,10 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
152
167
  send: "Send",
153
168
  stop: "Stop",
154
169
  attachFiles: "Attach files",
170
+ recordVoice: "Record voice",
171
+ stopRecording: "Stop recording",
172
+ transcribing: "Transcribing…",
173
+ transcriptionFailed: "Transcription failed",
155
174
 
156
175
  toolRunning: "running…",
157
176
  toolDone: "✓ done",
@@ -0,0 +1,169 @@
1
+ import type { TranscribeHandler } from "../core/transcribe_audio.js";
2
+ import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
3
+
4
+ /** Lifecycle of the mic button, reflected on its `data-state` for CSS. */
5
+ type VoiceState = "idle" | "recording" | "transcribing";
6
+
7
+ /** Construction options for {@link VoiceInput}. */
8
+ export interface VoiceInputOptions {
9
+ /** Turns a recorded clip into text (the built-in or a custom transport). */
10
+ readonly transcribe: TranscribeHandler;
11
+ /** Called with the transcript when a recording transcribes successfully. */
12
+ readonly onText: (text: string) => void;
13
+ /** UI string table (labels/tooltips). */
14
+ readonly strings?: UiStrings;
15
+ }
16
+
17
+ /**
18
+ * The composer's voice-input control: a mic button that records via
19
+ * `MediaRecorder`, then POSTs the clip through a {@link TranscribeHandler} and
20
+ * hands the transcript back via `onText`.
21
+ *
22
+ * Click to start recording (browser mic permission prompt), click again to stop
23
+ * — the clip is transcribed and dropped into the composer. The button reflects
24
+ * its `idle` / `recording` / `transcribing` state on `data-state` for theming
25
+ * and is exposed as `part="voice-button"`. A capture or transcription failure
26
+ * returns the button to idle and surfaces the message on its tooltip.
27
+ *
28
+ * Pure DOM (no framework); the host mounts {@link element} in the input row.
29
+ */
30
+ export class VoiceInput {
31
+ /** The mic button; mount this in the composer. */
32
+ readonly element: HTMLButtonElement;
33
+
34
+ readonly #transcribe: TranscribeHandler;
35
+ readonly #onText: (text: string) => void;
36
+ readonly #strings: UiStrings;
37
+ #state: VoiceState = "idle";
38
+ #recorder: MediaRecorder | null = null;
39
+ #stream: MediaStream | null = null;
40
+ #chunks: Blob[] = [];
41
+ #disposed = false;
42
+
43
+ constructor(options: VoiceInputOptions) {
44
+ this.#transcribe = options.transcribe;
45
+ this.#onText = options.onText;
46
+ this.#strings = options.strings ?? DEFAULT_UI_STRINGS;
47
+
48
+ this.element = document.createElement("button");
49
+ this.element.type = "button";
50
+ this.element.className = "voice-btn";
51
+ this.element.setAttribute("part", "voice-button");
52
+ this.element.textContent = "🎤";
53
+ this.#setState("idle");
54
+ this.element.addEventListener("click", () => {
55
+ void this.toggle();
56
+ });
57
+ }
58
+
59
+ /** Start recording when idle, stop (and transcribe) when recording. */
60
+ async toggle(): Promise<void> {
61
+ if (this.#state === "recording") {
62
+ this.#stop();
63
+ return;
64
+ }
65
+ if (this.#state === "transcribing") {
66
+ return;
67
+ }
68
+ await this.#start();
69
+ }
70
+
71
+ async #start(): Promise<void> {
72
+ let stream: MediaStream;
73
+ try {
74
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true });
75
+ } catch {
76
+ this.#fail(this.#strings.transcriptionFailed);
77
+ return;
78
+ }
79
+ this.#stream = stream;
80
+ this.#chunks = [];
81
+ const recorder = new MediaRecorder(stream);
82
+ recorder.addEventListener("dataavailable", (event) => {
83
+ this.#chunks.push(event.data);
84
+ });
85
+ recorder.addEventListener("stop", () => {
86
+ void this.#finish(recorder.mimeType);
87
+ });
88
+ this.#recorder = recorder;
89
+ recorder.start();
90
+ this.#setState("recording");
91
+ }
92
+
93
+ #stop(): void {
94
+ // ``stop`` flushes a final ``dataavailable`` then fires ``stop`` → #finish.
95
+ this.#recorder?.stop();
96
+ }
97
+
98
+ /**
99
+ * Tear the control down — the teardown path when the host element is removed
100
+ * mid-recording. Stops any live `MediaRecorder`, releases the mic tracks (so
101
+ * the browser's recording indicator clears), and suppresses the pending
102
+ * transcription: a disconnected control must not fire `onText` back into a
103
+ * detached element.
104
+ */
105
+ dispose(): void {
106
+ this.#disposed = true;
107
+ if (this.#recorder !== null && this.#recorder.state !== "inactive") {
108
+ this.#recorder.stop();
109
+ }
110
+ this.#recorder = null;
111
+ this.#releaseStream();
112
+ }
113
+
114
+ async #finish(mimeType: string): Promise<void> {
115
+ if (this.#disposed) {
116
+ return;
117
+ }
118
+ this.#releaseStream();
119
+ this.#setState("transcribing");
120
+ const audio = new Blob(this.#chunks, { type: mimeType || "audio/webm" });
121
+ try {
122
+ const text = await this.#transcribe(audio);
123
+ this.#setState("idle");
124
+ if (text !== "") {
125
+ this.#onText(text);
126
+ }
127
+ } catch (error) {
128
+ this.#fail(error instanceof Error ? error.message : this.#strings.transcriptionFailed);
129
+ } finally {
130
+ this.#recorder = null;
131
+ }
132
+ }
133
+
134
+ /** Stop the mic tracks so the browser's recording indicator clears. */
135
+ #releaseStream(): void {
136
+ for (const track of this.#stream?.getTracks() ?? []) {
137
+ track.stop();
138
+ }
139
+ this.#stream = null;
140
+ }
141
+
142
+ #fail(message: string): void {
143
+ this.#releaseStream();
144
+ this.#recorder = null;
145
+ this.#setState("idle");
146
+ this.element.title = message;
147
+ }
148
+
149
+ #setState(state: VoiceState): void {
150
+ this.#state = state;
151
+ this.element.dataset["state"] = state;
152
+ const label = this.#labelFor(state);
153
+ this.element.title = label;
154
+ this.element.setAttribute("aria-label", label);
155
+ this.element.setAttribute("aria-pressed", String(state === "recording"));
156
+ // The control is inert while a clip transcribes (no second recording yet).
157
+ this.element.disabled = state === "transcribing";
158
+ }
159
+
160
+ #labelFor(state: VoiceState): string {
161
+ if (state === "recording") {
162
+ return this.#strings.stopRecording;
163
+ }
164
+ if (state === "transcribing") {
165
+ return this.#strings.transcribing;
166
+ }
167
+ return this.#strings.recordVoice;
168
+ }
169
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.8.1";
1
+ export const VERSION: string = "0.10.0";