vanilla-agent 1.5.0 → 1.6.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 +43 -5
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -23
- package/dist/index.d.ts +163 -23
- package/dist/index.global.js +52 -52
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/dist/widget.css +25 -8
- package/package.json +1 -1
- package/src/client.ts +115 -21
- package/src/components/message-bubble.ts +6 -1
- package/src/components/reasoning-bubble.ts +2 -0
- package/src/components/tool-bubble.ts +2 -0
- package/src/index.ts +6 -0
- package/src/runtime/init.ts +5 -33
- package/src/session.ts +15 -0
- package/src/styles/widget.css +25 -8
- package/src/types.ts +140 -1
- package/src/ui.ts +414 -20
- package/src/utils/actions.ts +228 -0
- package/src/utils/events.ts +41 -0
- package/src/utils/formatting.ts +22 -10
- package/src/utils/storage.ts +72 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentWidgetActionContext,
|
|
3
|
+
AgentWidgetActionEventPayload,
|
|
4
|
+
AgentWidgetActionHandler,
|
|
5
|
+
AgentWidgetActionHandlerResult,
|
|
6
|
+
AgentWidgetActionParser,
|
|
7
|
+
AgentWidgetParsedAction,
|
|
8
|
+
AgentWidgetControllerEventMap,
|
|
9
|
+
AgentWidgetMessage
|
|
10
|
+
} from "../types";
|
|
11
|
+
|
|
12
|
+
type ActionManagerProcessContext = {
|
|
13
|
+
text: string;
|
|
14
|
+
message: AgentWidgetMessage;
|
|
15
|
+
streaming: boolean;
|
|
16
|
+
raw?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ActionManagerOptions = {
|
|
20
|
+
parsers: AgentWidgetActionParser[];
|
|
21
|
+
handlers: AgentWidgetActionHandler[];
|
|
22
|
+
getMetadata: () => Record<string, unknown>;
|
|
23
|
+
updateMetadata: (
|
|
24
|
+
updater: (prev: Record<string, unknown>) => Record<string, unknown>
|
|
25
|
+
) => void;
|
|
26
|
+
emit: <K extends keyof AgentWidgetControllerEventMap>(
|
|
27
|
+
event: K,
|
|
28
|
+
payload: AgentWidgetControllerEventMap[K]
|
|
29
|
+
) => void;
|
|
30
|
+
documentRef: Document | null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const stripCodeFence = (value: string) => {
|
|
34
|
+
const match = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
35
|
+
return match ? match[1] : value;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const extractJsonObject = (value: string) => {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
const start = trimmed.indexOf("{");
|
|
41
|
+
if (start === -1) return null;
|
|
42
|
+
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (let i = start; i < trimmed.length; i += 1) {
|
|
45
|
+
const char = trimmed[i];
|
|
46
|
+
if (char === "{") depth += 1;
|
|
47
|
+
if (char === "}") {
|
|
48
|
+
depth -= 1;
|
|
49
|
+
if (depth === 0) {
|
|
50
|
+
return trimmed.slice(start, i + 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const defaultJsonActionParser: AgentWidgetActionParser = ({ text }) => {
|
|
58
|
+
if (!text) return null;
|
|
59
|
+
if (!text.includes("{")) return null;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const withoutFence = stripCodeFence(text);
|
|
63
|
+
const jsonBody = extractJsonObject(withoutFence);
|
|
64
|
+
if (!jsonBody) return null;
|
|
65
|
+
const parsed = JSON.parse(jsonBody);
|
|
66
|
+
if (!parsed || typeof parsed !== "object" || !parsed.action) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const { action, ...payload } = parsed;
|
|
70
|
+
return {
|
|
71
|
+
type: String(action),
|
|
72
|
+
payload,
|
|
73
|
+
raw: parsed
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const asString = (value: unknown) =>
|
|
81
|
+
typeof value === "string" ? value : value == null ? "" : String(value);
|
|
82
|
+
|
|
83
|
+
export const defaultActionHandlers: Record<
|
|
84
|
+
string,
|
|
85
|
+
AgentWidgetActionHandler
|
|
86
|
+
> = {
|
|
87
|
+
message: (action) => {
|
|
88
|
+
if (action.type !== "message") return;
|
|
89
|
+
const text = asString((action.payload as Record<string, unknown>).text);
|
|
90
|
+
return {
|
|
91
|
+
handled: true,
|
|
92
|
+
displayText: text
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
messageAndClick: (action, context) => {
|
|
96
|
+
if (action.type !== "message_and_click") return;
|
|
97
|
+
const payload = action.payload as Record<string, unknown>;
|
|
98
|
+
const selector = asString(payload.element);
|
|
99
|
+
if (selector && context.document?.querySelector) {
|
|
100
|
+
const element = context.document.querySelector<HTMLElement>(selector);
|
|
101
|
+
if (element) {
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
element.click();
|
|
104
|
+
}, 400);
|
|
105
|
+
} else if (typeof console !== "undefined") {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.warn("[AgentWidget] Element not found for selector:", selector);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
handled: true,
|
|
112
|
+
displayText: asString(payload.text)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const ensureArrayOfStrings = (value: unknown): string[] => {
|
|
118
|
+
if (Array.isArray(value)) {
|
|
119
|
+
return value.map((entry) => String(entry));
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const createActionManager = (options: ActionManagerOptions) => {
|
|
125
|
+
let processedIds = new Set(
|
|
126
|
+
ensureArrayOfStrings(options.getMetadata().processedActionMessageIds)
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const syncFromMetadata = () => {
|
|
130
|
+
processedIds = new Set(
|
|
131
|
+
ensureArrayOfStrings(options.getMetadata().processedActionMessageIds)
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const persistProcessedIds = () => {
|
|
136
|
+
const latestIds = Array.from(processedIds);
|
|
137
|
+
options.updateMetadata((prev) => ({
|
|
138
|
+
...prev,
|
|
139
|
+
processedActionMessageIds: latestIds
|
|
140
|
+
}));
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const process = (context: ActionManagerProcessContext): string | null => {
|
|
144
|
+
if (
|
|
145
|
+
context.streaming ||
|
|
146
|
+
context.message.role !== "assistant" ||
|
|
147
|
+
!context.text ||
|
|
148
|
+
processedIds.has(context.message.id)
|
|
149
|
+
) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const parseSource =
|
|
154
|
+
(typeof context.raw === "string" && context.raw) ||
|
|
155
|
+
(typeof context.message.rawContent === "string" &&
|
|
156
|
+
context.message.rawContent) ||
|
|
157
|
+
(typeof context.text === "string" && context.text) ||
|
|
158
|
+
null;
|
|
159
|
+
|
|
160
|
+
if (
|
|
161
|
+
!parseSource &&
|
|
162
|
+
typeof context.text === "string" &&
|
|
163
|
+
context.text.trim().startsWith("{") &&
|
|
164
|
+
typeof console !== "undefined"
|
|
165
|
+
) {
|
|
166
|
+
// eslint-disable-next-line no-console
|
|
167
|
+
console.warn(
|
|
168
|
+
"[AgentWidget] Structured response detected but no raw payload was provided. Ensure your stream parser returns { text, raw }."
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const action = parseSource
|
|
173
|
+
? options.parsers.reduce<AgentWidgetParsedAction | null>(
|
|
174
|
+
(acc, parser) =>
|
|
175
|
+
acc || parser?.({ text: parseSource, message: context.message }) || null,
|
|
176
|
+
null
|
|
177
|
+
)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
if (!action) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
processedIds.add(context.message.id);
|
|
185
|
+
persistProcessedIds();
|
|
186
|
+
|
|
187
|
+
const eventPayload: AgentWidgetActionEventPayload = {
|
|
188
|
+
action,
|
|
189
|
+
message: context.message
|
|
190
|
+
};
|
|
191
|
+
options.emit("action:detected", eventPayload);
|
|
192
|
+
|
|
193
|
+
for (const handler of options.handlers) {
|
|
194
|
+
if (!handler) continue;
|
|
195
|
+
try {
|
|
196
|
+
const handlerResult = handler(action, {
|
|
197
|
+
message: context.message,
|
|
198
|
+
metadata: options.getMetadata(),
|
|
199
|
+
updateMetadata: options.updateMetadata,
|
|
200
|
+
document: options.documentRef
|
|
201
|
+
} as AgentWidgetActionContext) as AgentWidgetActionHandlerResult | void;
|
|
202
|
+
|
|
203
|
+
if (!handlerResult) continue;
|
|
204
|
+
|
|
205
|
+
if (handlerResult.displayText !== undefined && handlerResult.handled) {
|
|
206
|
+
return handlerResult.displayText;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (handlerResult.handled) {
|
|
210
|
+
return "";
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (typeof console !== "undefined") {
|
|
214
|
+
// eslint-disable-next-line no-console
|
|
215
|
+
console.error("[AgentWidget] Action handler error:", error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return "";
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
process,
|
|
225
|
+
syncFromMetadata
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type Handler<T> = (payload: T) => void;
|
|
2
|
+
|
|
3
|
+
export type EventUnsubscribe = () => void;
|
|
4
|
+
|
|
5
|
+
export const createEventBus = <EventMap extends Record<string, any>>() => {
|
|
6
|
+
const listeners = new Map<keyof EventMap, Set<Handler<any>>>();
|
|
7
|
+
|
|
8
|
+
const on = <K extends keyof EventMap>(
|
|
9
|
+
event: K,
|
|
10
|
+
handler: Handler<EventMap[K]>
|
|
11
|
+
): EventUnsubscribe => {
|
|
12
|
+
if (!listeners.has(event)) {
|
|
13
|
+
listeners.set(event, new Set());
|
|
14
|
+
}
|
|
15
|
+
listeners.get(event)!.add(handler as Handler<any>);
|
|
16
|
+
return () => off(event, handler);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const off = <K extends keyof EventMap>(
|
|
20
|
+
event: K,
|
|
21
|
+
handler: Handler<EventMap[K]>
|
|
22
|
+
) => {
|
|
23
|
+
listeners.get(event)?.delete(handler as Handler<any>);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const emit = <K extends keyof EventMap>(event: K, payload: EventMap[K]) => {
|
|
27
|
+
listeners.get(event)?.forEach((handler) => {
|
|
28
|
+
try {
|
|
29
|
+
handler(payload);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (typeof console !== "undefined") {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error("[AgentWidget] Event handler error:", error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return { on, off, emit };
|
|
40
|
+
};
|
|
41
|
+
|
package/src/utils/formatting.ts
CHANGED
|
@@ -140,7 +140,9 @@ const createRegexJsonParserInternal = (): {
|
|
|
140
140
|
processChunk: async (accumulatedContent: string): Promise<AgentWidgetStreamParserResult | string | null> => {
|
|
141
141
|
// Skip if no new content
|
|
142
142
|
if (accumulatedContent.length <= processedLength) {
|
|
143
|
-
return extractedText
|
|
143
|
+
return extractedText !== null
|
|
144
|
+
? { text: extractedText, raw: accumulatedContent }
|
|
145
|
+
: null;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
// Validate that the accumulated content looks like valid JSON
|
|
@@ -159,10 +161,14 @@ const createRegexJsonParserInternal = (): {
|
|
|
159
161
|
processedLength = accumulatedContent.length;
|
|
160
162
|
|
|
161
163
|
// Return both the extracted text and raw JSON
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
if (extractedText !== null) {
|
|
165
|
+
return {
|
|
166
|
+
text: extractedText,
|
|
167
|
+
raw: accumulatedContent
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
166
172
|
},
|
|
167
173
|
close: async () => {
|
|
168
174
|
// No cleanup needed for regex-based parser
|
|
@@ -257,7 +263,9 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
257
263
|
|
|
258
264
|
// Skip if no new content
|
|
259
265
|
if (accumulatedContent.length <= processedLength) {
|
|
260
|
-
return extractedText
|
|
266
|
+
return extractedText !== null
|
|
267
|
+
? { text: extractedText, raw: accumulatedContent }
|
|
268
|
+
: null;
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
try {
|
|
@@ -278,10 +286,14 @@ export const createJsonStreamParser = (): AgentWidgetStreamParser => {
|
|
|
278
286
|
processedLength = accumulatedContent.length;
|
|
279
287
|
|
|
280
288
|
// Return both the extracted text and raw JSON
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
if (extractedText !== null) {
|
|
290
|
+
return {
|
|
291
|
+
text: extractedText,
|
|
292
|
+
raw: accumulatedContent
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null;
|
|
285
297
|
},
|
|
286
298
|
close: () => {
|
|
287
299
|
// No cleanup needed
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentWidgetMessage,
|
|
3
|
+
AgentWidgetStorageAdapter,
|
|
4
|
+
AgentWidgetStoredState
|
|
5
|
+
} from "../types";
|
|
6
|
+
|
|
7
|
+
const safeJsonParse = (value: string | null) => {
|
|
8
|
+
if (!value) return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (typeof console !== "undefined") {
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
console.error("[AgentWidget] Failed to parse stored state:", error);
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sanitizeMessages = (messages: AgentWidgetMessage[]) =>
|
|
21
|
+
messages.map((message) => ({
|
|
22
|
+
...message,
|
|
23
|
+
streaming: false
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
export const createLocalStorageAdapter = (
|
|
27
|
+
key = "vanilla-agent-state"
|
|
28
|
+
): AgentWidgetStorageAdapter => {
|
|
29
|
+
const getStorage = () => {
|
|
30
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return window.localStorage;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
load: () => {
|
|
38
|
+
const storage = getStorage();
|
|
39
|
+
if (!storage) return null;
|
|
40
|
+
return safeJsonParse(storage.getItem(key));
|
|
41
|
+
},
|
|
42
|
+
save: (state: AgentWidgetStoredState) => {
|
|
43
|
+
const storage = getStorage();
|
|
44
|
+
if (!storage) return;
|
|
45
|
+
try {
|
|
46
|
+
const payload: AgentWidgetStoredState = {
|
|
47
|
+
...state,
|
|
48
|
+
messages: state.messages ? sanitizeMessages(state.messages) : undefined
|
|
49
|
+
};
|
|
50
|
+
storage.setItem(key, JSON.stringify(payload));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (typeof console !== "undefined") {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.error("[AgentWidget] Failed to persist state:", error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
clear: () => {
|
|
59
|
+
const storage = getStorage();
|
|
60
|
+
if (!storage) return;
|
|
61
|
+
try {
|
|
62
|
+
storage.removeItem(key);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (typeof console !== "undefined") {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.error("[AgentWidget] Failed to clear stored state:", error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|