vanilla-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -0
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +370 -0
- package/dist/index.d.ts +370 -0
- package/dist/index.global.js +1710 -0
- package/dist/index.global.js.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/install.global.js +2 -0
- package/dist/install.global.js.map +1 -0
- package/dist/widget.css +752 -0
- package/package.json +61 -0
- package/src/client.ts +577 -0
- package/src/components/forms.ts +165 -0
- package/src/components/launcher.ts +184 -0
- package/src/components/message-bubble.ts +52 -0
- package/src/components/messages.ts +43 -0
- package/src/components/panel.ts +555 -0
- package/src/components/reasoning-bubble.ts +114 -0
- package/src/components/suggestions.ts +52 -0
- package/src/components/tool-bubble.ts +158 -0
- package/src/index.ts +37 -0
- package/src/install.ts +159 -0
- package/src/plugins/registry.ts +72 -0
- package/src/plugins/types.ts +90 -0
- package/src/postprocessors.ts +76 -0
- package/src/runtime/init.ts +116 -0
- package/src/session.ts +206 -0
- package/src/styles/tailwind.css +19 -0
- package/src/styles/widget.css +752 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +1325 -0
- package/src/utils/constants.ts +11 -0
- package/src/utils/dom.ts +20 -0
- package/src/utils/formatting.ts +77 -0
- package/src/utils/icons.ts +92 -0
- package/src/utils/positioning.ts +12 -0
- package/src/utils/theme.ts +20 -0
- package/src/widget.css +1 -0
- package/widget.css +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createChatExperience, ChatWidgetController } from "../ui";
|
|
2
|
+
import { ChatWidgetConfig, ChatWidgetInitOptions } from "../types";
|
|
3
|
+
|
|
4
|
+
const ensureTarget = (target: string | HTMLElement): HTMLElement => {
|
|
5
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
6
|
+
throw new Error("Chat widget can only be mounted in a browser environment");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof target === "string") {
|
|
10
|
+
const element = document.querySelector<HTMLElement>(target);
|
|
11
|
+
if (!element) {
|
|
12
|
+
throw new Error(`Chat widget target "${target}" was not found`);
|
|
13
|
+
}
|
|
14
|
+
return element;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const widgetCssHref = (): string | null => {
|
|
21
|
+
try {
|
|
22
|
+
// This works in ESM builds but not in IIFE builds
|
|
23
|
+
if (typeof import.meta !== "undefined" && import.meta.url) {
|
|
24
|
+
return new URL("../widget.css", import.meta.url).href;
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// Fallback for IIFE builds where CSS should be loaded separately
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const mountStyles = (root: ShadowRoot | HTMLElement) => {
|
|
33
|
+
const href = widgetCssHref();
|
|
34
|
+
|
|
35
|
+
if (root instanceof ShadowRoot) {
|
|
36
|
+
// For shadow DOM, we need to load CSS into the shadow root
|
|
37
|
+
if (href) {
|
|
38
|
+
const link = document.createElement("link");
|
|
39
|
+
link.rel = "stylesheet";
|
|
40
|
+
link.href = href;
|
|
41
|
+
link.setAttribute("data-vanilla-agent", "true");
|
|
42
|
+
root.insertBefore(link, root.firstChild);
|
|
43
|
+
}
|
|
44
|
+
// If href is null (IIFE build), CSS should already be loaded globally
|
|
45
|
+
} else {
|
|
46
|
+
// For non-shadow DOM, check if CSS is already loaded
|
|
47
|
+
const existing = document.head.querySelector<HTMLLinkElement>(
|
|
48
|
+
"link[data-vanilla-agent]"
|
|
49
|
+
);
|
|
50
|
+
if (!existing) {
|
|
51
|
+
if (href) {
|
|
52
|
+
// ESM build - load CSS dynamically
|
|
53
|
+
const link = document.createElement("link");
|
|
54
|
+
link.rel = "stylesheet";
|
|
55
|
+
link.href = href;
|
|
56
|
+
link.setAttribute("data-vanilla-agent", "true");
|
|
57
|
+
document.head.appendChild(link);
|
|
58
|
+
}
|
|
59
|
+
// IIFE build - CSS should be loaded via <link> tag before script
|
|
60
|
+
// If not found, we'll assume it's loaded globally or warn in dev
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ChatWidgetInitHandle = ChatWidgetController & { host: HTMLElement };
|
|
66
|
+
|
|
67
|
+
export const initChatWidget = (
|
|
68
|
+
options: ChatWidgetInitOptions
|
|
69
|
+
): ChatWidgetInitHandle => {
|
|
70
|
+
const target = ensureTarget(options.target);
|
|
71
|
+
const host = document.createElement("div");
|
|
72
|
+
host.className = "vanilla-agent-host";
|
|
73
|
+
target.appendChild(host);
|
|
74
|
+
|
|
75
|
+
const useShadow = options.useShadowDom !== false;
|
|
76
|
+
let mount: HTMLElement;
|
|
77
|
+
let root: ShadowRoot | HTMLElement;
|
|
78
|
+
|
|
79
|
+
if (useShadow) {
|
|
80
|
+
const shadowRoot = host.attachShadow({ mode: "open" });
|
|
81
|
+
root = shadowRoot;
|
|
82
|
+
mount = document.createElement("div");
|
|
83
|
+
mount.id = "vanilla-agent-root";
|
|
84
|
+
shadowRoot.appendChild(mount);
|
|
85
|
+
mountStyles(shadowRoot);
|
|
86
|
+
} else {
|
|
87
|
+
root = host;
|
|
88
|
+
mount = document.createElement("div");
|
|
89
|
+
mount.id = "vanilla-agent-root";
|
|
90
|
+
host.appendChild(mount);
|
|
91
|
+
mountStyles(host);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let controller = createChatExperience(mount, options.config);
|
|
95
|
+
options.onReady?.();
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
host,
|
|
99
|
+
update(nextConfig: ChatWidgetConfig) {
|
|
100
|
+
controller.update(nextConfig);
|
|
101
|
+
},
|
|
102
|
+
open() {
|
|
103
|
+
controller.open();
|
|
104
|
+
},
|
|
105
|
+
close() {
|
|
106
|
+
controller.close();
|
|
107
|
+
},
|
|
108
|
+
toggle() {
|
|
109
|
+
controller.toggle();
|
|
110
|
+
},
|
|
111
|
+
destroy() {
|
|
112
|
+
controller.destroy();
|
|
113
|
+
host.remove();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { ChatWidgetClient } from "./client";
|
|
2
|
+
import {
|
|
3
|
+
ChatWidgetConfig,
|
|
4
|
+
ChatWidgetEvent,
|
|
5
|
+
ChatWidgetMessage
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export type ChatWidgetSessionStatus =
|
|
9
|
+
| "idle"
|
|
10
|
+
| "connecting"
|
|
11
|
+
| "connected"
|
|
12
|
+
| "error";
|
|
13
|
+
|
|
14
|
+
type SessionCallbacks = {
|
|
15
|
+
onMessagesChanged: (messages: ChatWidgetMessage[]) => void;
|
|
16
|
+
onStatusChanged: (status: ChatWidgetSessionStatus) => void;
|
|
17
|
+
onStreamingChanged: (streaming: boolean) => void;
|
|
18
|
+
onError?: (error: Error) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class ChatWidgetSession {
|
|
22
|
+
private client: ChatWidgetClient;
|
|
23
|
+
private messages: ChatWidgetMessage[];
|
|
24
|
+
private status: ChatWidgetSessionStatus = "idle";
|
|
25
|
+
private streaming = false;
|
|
26
|
+
private abortController: AbortController | null = null;
|
|
27
|
+
private sequenceCounter = Date.now();
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private config: ChatWidgetConfig = {},
|
|
31
|
+
private callbacks: SessionCallbacks
|
|
32
|
+
) {
|
|
33
|
+
this.messages = [...(config.initialMessages ?? [])].map((message) => ({
|
|
34
|
+
...message,
|
|
35
|
+
sequence: message.sequence ?? this.nextSequence()
|
|
36
|
+
}));
|
|
37
|
+
this.messages = this.sortMessages(this.messages);
|
|
38
|
+
this.client = new ChatWidgetClient(config);
|
|
39
|
+
|
|
40
|
+
if (this.messages.length) {
|
|
41
|
+
this.callbacks.onMessagesChanged([...this.messages]);
|
|
42
|
+
}
|
|
43
|
+
this.callbacks.onStatusChanged(this.status);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public updateConfig(next: ChatWidgetConfig) {
|
|
47
|
+
this.config = { ...this.config, ...next };
|
|
48
|
+
this.client = new ChatWidgetClient(this.config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getMessages() {
|
|
52
|
+
return [...this.messages];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public getStatus() {
|
|
56
|
+
return this.status;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public isStreaming() {
|
|
60
|
+
return this.streaming;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async sendMessage(rawInput: string) {
|
|
64
|
+
const input = rawInput.trim();
|
|
65
|
+
if (!input) return;
|
|
66
|
+
|
|
67
|
+
this.abortController?.abort();
|
|
68
|
+
|
|
69
|
+
const userMessage: ChatWidgetMessage = {
|
|
70
|
+
id: `user-${Date.now()}`,
|
|
71
|
+
role: "user",
|
|
72
|
+
content: input,
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
sequence: this.nextSequence()
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.appendMessage(userMessage);
|
|
78
|
+
this.setStreaming(true);
|
|
79
|
+
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
this.abortController = controller;
|
|
82
|
+
|
|
83
|
+
const snapshot = [...this.messages];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await this.client.dispatch(
|
|
87
|
+
{
|
|
88
|
+
messages: snapshot,
|
|
89
|
+
signal: controller.signal
|
|
90
|
+
},
|
|
91
|
+
this.handleEvent
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const fallback: ChatWidgetMessage = {
|
|
95
|
+
id: `assistant-${Date.now()}`,
|
|
96
|
+
role: "assistant",
|
|
97
|
+
createdAt: new Date().toISOString(),
|
|
98
|
+
content:
|
|
99
|
+
"It looks like the proxy isn't returning a real response yet. Here's a sample message so you can continue testing locally.",
|
|
100
|
+
sequence: this.nextSequence()
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
this.appendMessage(fallback);
|
|
104
|
+
this.setStatus("idle");
|
|
105
|
+
this.setStreaming(false);
|
|
106
|
+
this.abortController = null;
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
this.callbacks.onError?.(error);
|
|
109
|
+
} else {
|
|
110
|
+
this.callbacks.onError?.(new Error(String(error)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public cancel() {
|
|
116
|
+
this.abortController?.abort();
|
|
117
|
+
this.abortController = null;
|
|
118
|
+
this.setStreaming(false);
|
|
119
|
+
this.setStatus("idle");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private handleEvent = (event: ChatWidgetEvent) => {
|
|
123
|
+
if (event.type === "message") {
|
|
124
|
+
this.upsertMessage(event.message);
|
|
125
|
+
} else if (event.type === "status") {
|
|
126
|
+
this.setStatus(event.status);
|
|
127
|
+
if (event.status === "connecting") {
|
|
128
|
+
this.setStreaming(true);
|
|
129
|
+
} else if (event.status === "idle" || event.status === "error") {
|
|
130
|
+
this.setStreaming(false);
|
|
131
|
+
this.abortController = null;
|
|
132
|
+
}
|
|
133
|
+
} else if (event.type === "error") {
|
|
134
|
+
this.setStatus("error");
|
|
135
|
+
this.setStreaming(false);
|
|
136
|
+
this.abortController = null;
|
|
137
|
+
this.callbacks.onError?.(event.error);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
private setStatus(status: ChatWidgetSessionStatus) {
|
|
142
|
+
if (this.status === status) return;
|
|
143
|
+
this.status = status;
|
|
144
|
+
this.callbacks.onStatusChanged(status);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private setStreaming(streaming: boolean) {
|
|
148
|
+
if (this.streaming === streaming) return;
|
|
149
|
+
this.streaming = streaming;
|
|
150
|
+
this.callbacks.onStreamingChanged(streaming);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private appendMessage(message: ChatWidgetMessage) {
|
|
154
|
+
const withSequence = this.ensureSequence(message);
|
|
155
|
+
this.messages = this.sortMessages([...this.messages, withSequence]);
|
|
156
|
+
this.callbacks.onMessagesChanged([...this.messages]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private upsertMessage(message: ChatWidgetMessage) {
|
|
160
|
+
const withSequence = this.ensureSequence(message);
|
|
161
|
+
const index = this.messages.findIndex((m) => m.id === withSequence.id);
|
|
162
|
+
if (index === -1) {
|
|
163
|
+
this.appendMessage(withSequence);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.messages = this.messages.map((existing, idx) =>
|
|
168
|
+
idx === index ? { ...existing, ...withSequence } : existing
|
|
169
|
+
);
|
|
170
|
+
this.messages = this.sortMessages(this.messages);
|
|
171
|
+
this.callbacks.onMessagesChanged([...this.messages]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private ensureSequence(message: ChatWidgetMessage): ChatWidgetMessage {
|
|
175
|
+
if (message.sequence !== undefined) {
|
|
176
|
+
return { ...message };
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
...message,
|
|
180
|
+
sequence: this.nextSequence()
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private nextSequence() {
|
|
185
|
+
return this.sequenceCounter++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private sortMessages(messages: ChatWidgetMessage[]) {
|
|
189
|
+
return [...messages].sort((a, b) => {
|
|
190
|
+
// Sort by createdAt timestamp first (chronological order)
|
|
191
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
192
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
193
|
+
if (!Number.isNaN(timeA) && !Number.isNaN(timeB) && timeA !== timeB) {
|
|
194
|
+
return timeA - timeB;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Fall back to sequence if timestamps are equal or invalid
|
|
198
|
+
const seqA = a.sequence ?? 0;
|
|
199
|
+
const seqB = b.sequence ?? 0;
|
|
200
|
+
if (seqA !== seqB) return seqA - seqB;
|
|
201
|
+
|
|
202
|
+
// Final fallback to ID
|
|
203
|
+
return a.id.localeCompare(b.id);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
@layer base {
|
|
6
|
+
:root {
|
|
7
|
+
--travrse-primary: #111827;
|
|
8
|
+
--travrse-secondary: #4338ca;
|
|
9
|
+
--travrse-surface: #ffffff;
|
|
10
|
+
--travrse-muted: #6b7280;
|
|
11
|
+
--travrse-accent: #1d4ed8;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
.tvw-widget-shadow {
|
|
17
|
+
box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.35);
|
|
18
|
+
}
|
|
19
|
+
}
|