@surf-kit/agent 0.1.1

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/dist/hooks.cjs ADDED
@@ -0,0 +1,652 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/hooks.ts
21
+ var hooks_exports = {};
22
+ __export(hooks_exports, {
23
+ useAgentChat: () => useAgentChat,
24
+ useAgentTheme: () => useAgentTheme,
25
+ useCharacterDrain: () => useCharacterDrain,
26
+ useConversation: () => useConversation,
27
+ useFeedback: () => useFeedback,
28
+ useStreaming: () => useStreaming
29
+ });
30
+ module.exports = __toCommonJS(hooks_exports);
31
+
32
+ // src/hooks/useAgentChat.ts
33
+ var import_react = require("react");
34
+ var initialState = {
35
+ messages: [],
36
+ conversationId: null,
37
+ isLoading: false,
38
+ error: null,
39
+ inputValue: "",
40
+ streamPhase: "idle",
41
+ streamingContent: ""
42
+ };
43
+ function reducer(state, action) {
44
+ switch (action.type) {
45
+ case "SET_INPUT":
46
+ return { ...state, inputValue: action.value };
47
+ case "SEND_START":
48
+ return {
49
+ ...state,
50
+ messages: [...state.messages, action.message],
51
+ isLoading: true,
52
+ error: null,
53
+ inputValue: "",
54
+ streamPhase: "thinking",
55
+ streamingContent: ""
56
+ };
57
+ case "STREAM_PHASE":
58
+ return { ...state, streamPhase: action.phase };
59
+ case "STREAM_CONTENT":
60
+ return { ...state, streamingContent: state.streamingContent + action.content };
61
+ case "SEND_SUCCESS":
62
+ return {
63
+ ...state,
64
+ messages: [...state.messages, action.message],
65
+ conversationId: action.conversationId ?? state.conversationId,
66
+ isLoading: false,
67
+ streamPhase: "idle",
68
+ streamingContent: ""
69
+ };
70
+ case "SEND_ERROR":
71
+ return {
72
+ ...state,
73
+ isLoading: false,
74
+ error: action.error,
75
+ streamPhase: "idle",
76
+ streamingContent: ""
77
+ };
78
+ case "LOAD_CONVERSATION":
79
+ return {
80
+ ...state,
81
+ conversationId: action.conversationId,
82
+ messages: action.messages,
83
+ error: null
84
+ };
85
+ case "RESET":
86
+ return { ...initialState };
87
+ case "CLEAR_ERROR":
88
+ return { ...state, error: null };
89
+ default:
90
+ return state;
91
+ }
92
+ }
93
+ var msgIdCounter = 0;
94
+ function generateMessageId() {
95
+ return `msg-${Date.now()}-${++msgIdCounter}`;
96
+ }
97
+ function useAgentChat(config) {
98
+ const [state, dispatch] = (0, import_react.useReducer)(reducer, initialState);
99
+ const configRef = (0, import_react.useRef)(config);
100
+ configRef.current = config;
101
+ const lastUserMessageRef = (0, import_react.useRef)(null);
102
+ const sendMessage = (0, import_react.useCallback)(
103
+ async (content) => {
104
+ const { apiUrl, streamPath = "/chat/stream", headers = {}, timeout = 3e4 } = configRef.current;
105
+ lastUserMessageRef.current = content;
106
+ const userMessage = {
107
+ id: generateMessageId(),
108
+ role: "user",
109
+ content,
110
+ timestamp: /* @__PURE__ */ new Date()
111
+ };
112
+ dispatch({ type: "SEND_START", message: userMessage });
113
+ const controller = new AbortController();
114
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
115
+ try {
116
+ const response = await fetch(`${apiUrl}${streamPath}`, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ Accept: "text/event-stream",
121
+ ...headers
122
+ },
123
+ body: JSON.stringify({
124
+ message: content,
125
+ conversation_id: state.conversationId
126
+ }),
127
+ signal: controller.signal
128
+ });
129
+ clearTimeout(timeoutId);
130
+ if (!response.ok) {
131
+ dispatch({
132
+ type: "SEND_ERROR",
133
+ error: {
134
+ code: "API_ERROR",
135
+ message: `HTTP ${response.status}: ${response.statusText}`,
136
+ retryable: response.status >= 500
137
+ }
138
+ });
139
+ return;
140
+ }
141
+ const reader = response.body?.getReader();
142
+ if (!reader) {
143
+ dispatch({
144
+ type: "SEND_ERROR",
145
+ error: { code: "STREAM_ERROR", message: "No response body", retryable: true }
146
+ });
147
+ return;
148
+ }
149
+ const decoder = new TextDecoder();
150
+ let buffer = "";
151
+ let accumulatedContent = "";
152
+ let agentResponse = null;
153
+ let capturedAgent = null;
154
+ let capturedConversationId = null;
155
+ while (true) {
156
+ const { done, value } = await reader.read();
157
+ if (done) break;
158
+ buffer += decoder.decode(value, { stream: true });
159
+ const lines = buffer.split("\n");
160
+ buffer = lines.pop() ?? "";
161
+ for (const line of lines) {
162
+ if (!line.startsWith("data: ")) continue;
163
+ const data = line.slice(6).trim();
164
+ if (data === "[DONE]") continue;
165
+ try {
166
+ const event = JSON.parse(data);
167
+ switch (event.type) {
168
+ case "agent":
169
+ capturedAgent = event.agent;
170
+ break;
171
+ case "phase":
172
+ dispatch({ type: "STREAM_PHASE", phase: event.phase });
173
+ break;
174
+ case "delta":
175
+ accumulatedContent += event.content;
176
+ dispatch({ type: "STREAM_CONTENT", content: event.content });
177
+ break;
178
+ case "done":
179
+ agentResponse = event.response;
180
+ capturedConversationId = event.conversation_id ?? null;
181
+ break;
182
+ case "error":
183
+ dispatch({ type: "SEND_ERROR", error: event.error });
184
+ return;
185
+ }
186
+ } catch {
187
+ }
188
+ }
189
+ }
190
+ const assistantMessage = {
191
+ id: generateMessageId(),
192
+ role: "assistant",
193
+ content: agentResponse?.message ?? accumulatedContent,
194
+ response: agentResponse ?? void 0,
195
+ agent: capturedAgent ?? void 0,
196
+ timestamp: /* @__PURE__ */ new Date()
197
+ };
198
+ dispatch({
199
+ type: "SEND_SUCCESS",
200
+ message: assistantMessage,
201
+ streamingContent: accumulatedContent,
202
+ conversationId: capturedConversationId
203
+ });
204
+ } catch (err) {
205
+ clearTimeout(timeoutId);
206
+ if (err.name === "AbortError") {
207
+ dispatch({
208
+ type: "SEND_ERROR",
209
+ error: { code: "TIMEOUT", message: "Request timed out", retryable: true }
210
+ });
211
+ } else {
212
+ dispatch({
213
+ type: "SEND_ERROR",
214
+ error: {
215
+ code: "NETWORK_ERROR",
216
+ message: err.message ?? "Network error",
217
+ retryable: true
218
+ }
219
+ });
220
+ }
221
+ }
222
+ },
223
+ [state.conversationId]
224
+ );
225
+ const setInputValue = (0, import_react.useCallback)((value) => {
226
+ dispatch({ type: "SET_INPUT", value });
227
+ }, []);
228
+ const loadConversation = (0, import_react.useCallback)((conversationId, messages) => {
229
+ dispatch({ type: "LOAD_CONVERSATION", conversationId, messages });
230
+ }, []);
231
+ const submitFeedback = (0, import_react.useCallback)(
232
+ async (messageId, rating, comment) => {
233
+ const { apiUrl, feedbackPath = "/feedback", headers = {} } = configRef.current;
234
+ await fetch(`${apiUrl}${feedbackPath}`, {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json", ...headers },
237
+ body: JSON.stringify({ messageId, rating, comment })
238
+ });
239
+ },
240
+ []
241
+ );
242
+ const retry = (0, import_react.useCallback)(async () => {
243
+ if (lastUserMessageRef.current) {
244
+ await sendMessage(lastUserMessageRef.current);
245
+ }
246
+ }, [sendMessage]);
247
+ const reset = (0, import_react.useCallback)(() => {
248
+ dispatch({ type: "RESET" });
249
+ lastUserMessageRef.current = null;
250
+ }, []);
251
+ const actions = {
252
+ sendMessage,
253
+ setInputValue,
254
+ loadConversation,
255
+ submitFeedback,
256
+ retry,
257
+ reset
258
+ };
259
+ return { state, actions };
260
+ }
261
+
262
+ // src/hooks/useStreaming.ts
263
+ var import_react2 = require("react");
264
+ var initialState2 = {
265
+ active: false,
266
+ phase: "idle",
267
+ content: "",
268
+ sources: [],
269
+ agent: null,
270
+ agentLabel: null
271
+ };
272
+ function useStreaming(options) {
273
+ const { url, headers, onDone, onError } = options;
274
+ const [state, setState] = (0, import_react2.useState)(initialState2);
275
+ const abortRef = (0, import_react2.useRef)(null);
276
+ const optionsRef = (0, import_react2.useRef)(options);
277
+ optionsRef.current = options;
278
+ const stop = (0, import_react2.useCallback)(() => {
279
+ if (abortRef.current) {
280
+ abortRef.current.abort();
281
+ abortRef.current = null;
282
+ }
283
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
284
+ }, []);
285
+ const start = (0, import_react2.useCallback)(
286
+ async (body) => {
287
+ setState({ ...initialState2, active: true, phase: "thinking" });
288
+ const controller = new AbortController();
289
+ abortRef.current = controller;
290
+ try {
291
+ const response = await fetch(url, {
292
+ method: "POST",
293
+ headers: {
294
+ "Content-Type": "application/json",
295
+ Accept: "text/event-stream",
296
+ ...headers
297
+ },
298
+ body: JSON.stringify(body),
299
+ signal: controller.signal
300
+ });
301
+ if (!response.ok) {
302
+ const errorEvent = {
303
+ type: "error",
304
+ error: {
305
+ code: "API_ERROR",
306
+ message: `HTTP ${response.status}: ${response.statusText}`,
307
+ retryable: response.status >= 500
308
+ }
309
+ };
310
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
311
+ optionsRef.current.onError?.(errorEvent);
312
+ return;
313
+ }
314
+ const reader = response.body?.getReader();
315
+ if (!reader) {
316
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
317
+ return;
318
+ }
319
+ const decoder = new TextDecoder();
320
+ let buffer = "";
321
+ while (true) {
322
+ const { done, value } = await reader.read();
323
+ if (done) break;
324
+ buffer += decoder.decode(value, { stream: true });
325
+ const lines = buffer.split("\n");
326
+ buffer = lines.pop() ?? "";
327
+ for (const line of lines) {
328
+ if (!line.startsWith("data: ")) continue;
329
+ const data = line.slice(6).trim();
330
+ if (data === "[DONE]") continue;
331
+ try {
332
+ const event = JSON.parse(data);
333
+ processEvent(event, setState, optionsRef);
334
+ } catch {
335
+ }
336
+ }
337
+ }
338
+ } catch (err) {
339
+ if (err.name === "AbortError") return;
340
+ const errorEvent = {
341
+ type: "error",
342
+ error: {
343
+ code: "NETWORK_ERROR",
344
+ message: err.message ?? "Network error",
345
+ retryable: true
346
+ }
347
+ };
348
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
349
+ optionsRef.current.onError?.(errorEvent);
350
+ }
351
+ },
352
+ [url, headers]
353
+ );
354
+ (0, import_react2.useEffect)(() => {
355
+ return () => {
356
+ if (abortRef.current) {
357
+ abortRef.current.abort();
358
+ }
359
+ };
360
+ }, []);
361
+ return { state, start, stop };
362
+ }
363
+ function processEvent(event, setState, optionsRef) {
364
+ switch (event.type) {
365
+ case "phase":
366
+ setState((prev) => ({ ...prev, phase: event.phase }));
367
+ break;
368
+ case "delta":
369
+ setState((prev) => ({ ...prev, content: prev.content + event.content }));
370
+ break;
371
+ case "source":
372
+ setState((prev) => ({ ...prev, sources: [...prev.sources, event.source] }));
373
+ break;
374
+ case "agent":
375
+ setState((prev) => ({ ...prev, agent: event.agent }));
376
+ break;
377
+ case "done":
378
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
379
+ optionsRef.current.onDone?.(event);
380
+ break;
381
+ case "error":
382
+ setState((prev) => ({ ...prev, active: false, phase: "idle" }));
383
+ optionsRef.current.onError?.(event);
384
+ break;
385
+ }
386
+ }
387
+
388
+ // src/hooks/useConversation.ts
389
+ var import_react3 = require("react");
390
+ var STORAGE_PREFIX = "surf-kit-conversations";
391
+ function getStorageKey(prefix) {
392
+ return `${prefix}:list`;
393
+ }
394
+ function loadFromStorage(storageKey) {
395
+ if (typeof window === "undefined") return [];
396
+ try {
397
+ const raw = window.localStorage.getItem(getStorageKey(storageKey));
398
+ if (!raw) return [];
399
+ const parsed = JSON.parse(raw);
400
+ return parsed.map((c) => ({
401
+ ...c,
402
+ createdAt: new Date(c.createdAt),
403
+ updatedAt: new Date(c.updatedAt),
404
+ messages: c.messages.map((m) => ({ ...m, timestamp: new Date(m.timestamp) }))
405
+ }));
406
+ } catch {
407
+ return [];
408
+ }
409
+ }
410
+ function saveToStorage(storageKey, conversations) {
411
+ if (typeof window === "undefined") return;
412
+ try {
413
+ window.localStorage.setItem(getStorageKey(storageKey), JSON.stringify(conversations));
414
+ } catch {
415
+ }
416
+ }
417
+ var idCounter = 0;
418
+ function generateId() {
419
+ return `conv-${Date.now()}-${++idCounter}`;
420
+ }
421
+ function useConversation(options = {}) {
422
+ const { persist = false, storageKey = STORAGE_PREFIX } = options;
423
+ const [conversations, setConversations] = (0, import_react3.useState)(
424
+ () => persist ? loadFromStorage(storageKey) : []
425
+ );
426
+ const [current, setCurrent] = (0, import_react3.useState)(null);
427
+ const persistIfNeeded = (0, import_react3.useCallback)(
428
+ (convs) => {
429
+ if (persist) saveToStorage(storageKey, convs);
430
+ },
431
+ [persist, storageKey]
432
+ );
433
+ const create = (0, import_react3.useCallback)(
434
+ (title) => {
435
+ const now = /* @__PURE__ */ new Date();
436
+ const conv = {
437
+ id: generateId(),
438
+ title: title ?? "New Conversation",
439
+ messages: [],
440
+ createdAt: now,
441
+ updatedAt: now
442
+ };
443
+ setConversations((prev) => {
444
+ const next = [conv, ...prev];
445
+ persistIfNeeded(next);
446
+ return next;
447
+ });
448
+ setCurrent(conv);
449
+ return conv;
450
+ },
451
+ [persistIfNeeded]
452
+ );
453
+ const list = (0, import_react3.useCallback)(() => {
454
+ return conversations.map((c) => ({
455
+ id: c.id,
456
+ title: c.title,
457
+ lastMessage: c.messages.length > 0 ? c.messages[c.messages.length - 1].content : "",
458
+ updatedAt: c.updatedAt,
459
+ messageCount: c.messages.length
460
+ }));
461
+ }, [conversations]);
462
+ const load = (0, import_react3.useCallback)(
463
+ (id) => {
464
+ const conv = conversations.find((c) => c.id === id) ?? null;
465
+ setCurrent(conv);
466
+ return conv;
467
+ },
468
+ [conversations]
469
+ );
470
+ const remove = (0, import_react3.useCallback)(
471
+ (id) => {
472
+ setConversations((prev) => {
473
+ const next = prev.filter((c) => c.id !== id);
474
+ persistIfNeeded(next);
475
+ return next;
476
+ });
477
+ setCurrent((prev) => prev?.id === id ? null : prev);
478
+ },
479
+ [persistIfNeeded]
480
+ );
481
+ const rename = (0, import_react3.useCallback)(
482
+ (id, title) => {
483
+ setConversations((prev) => {
484
+ const next = prev.map((c) => c.id === id ? { ...c, title, updatedAt: /* @__PURE__ */ new Date() } : c);
485
+ persistIfNeeded(next);
486
+ return next;
487
+ });
488
+ setCurrent((prev) => prev?.id === id ? { ...prev, title, updatedAt: /* @__PURE__ */ new Date() } : prev);
489
+ },
490
+ [persistIfNeeded]
491
+ );
492
+ const addMessage = (0, import_react3.useCallback)(
493
+ (conversationId, message) => {
494
+ setConversations((prev) => {
495
+ const next = prev.map(
496
+ (c) => c.id === conversationId ? { ...c, messages: [...c.messages, message], updatedAt: /* @__PURE__ */ new Date() } : c
497
+ );
498
+ persistIfNeeded(next);
499
+ return next;
500
+ });
501
+ setCurrent(
502
+ (prev) => prev?.id === conversationId ? { ...prev, messages: [...prev.messages, message], updatedAt: /* @__PURE__ */ new Date() } : prev
503
+ );
504
+ },
505
+ [persistIfNeeded]
506
+ );
507
+ return {
508
+ conversations,
509
+ current,
510
+ create,
511
+ list,
512
+ load,
513
+ delete: remove,
514
+ rename,
515
+ addMessage
516
+ };
517
+ }
518
+
519
+ // src/hooks/useFeedback.ts
520
+ var import_react4 = require("react");
521
+ function useFeedback(options) {
522
+ const { url, headers } = options;
523
+ const [state, setState] = (0, import_react4.useState)("idle");
524
+ const [error, setError] = (0, import_react4.useState)(null);
525
+ const abortRef = (0, import_react4.useRef)(null);
526
+ const submit = (0, import_react4.useCallback)(
527
+ async (messageId, rating, comment) => {
528
+ setState("submitting");
529
+ setError(null);
530
+ const controller = new AbortController();
531
+ abortRef.current = controller;
532
+ try {
533
+ const response = await fetch(url, {
534
+ method: "POST",
535
+ headers: {
536
+ "Content-Type": "application/json",
537
+ ...headers
538
+ },
539
+ body: JSON.stringify({ messageId, rating, comment }),
540
+ signal: controller.signal
541
+ });
542
+ if (!response.ok) {
543
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
544
+ }
545
+ setState("submitted");
546
+ } catch (err) {
547
+ if (err.name === "AbortError") return;
548
+ setError(err.message ?? "Failed to submit feedback");
549
+ setState("error");
550
+ }
551
+ },
552
+ [url, headers]
553
+ );
554
+ const reset = (0, import_react4.useCallback)(() => {
555
+ setState("idle");
556
+ setError(null);
557
+ }, []);
558
+ return { state, error, submit, reset };
559
+ }
560
+
561
+ // src/hooks/useAgentTheme.ts
562
+ var import_react5 = require("react");
563
+ var DEFAULT_ACCENT = "#6366f1";
564
+ var DEFAULT_LABEL = "Agent";
565
+ function useAgentTheme(agentId, agentThemes) {
566
+ return (0, import_react5.useMemo)(() => {
567
+ if (!agentId) {
568
+ return { accent: DEFAULT_ACCENT, icon: null, label: DEFAULT_LABEL };
569
+ }
570
+ const theme = agentThemes?.[agentId];
571
+ if (!theme) {
572
+ return { accent: DEFAULT_ACCENT, icon: null, label: agentId };
573
+ }
574
+ return {
575
+ accent: theme.accent ?? DEFAULT_ACCENT,
576
+ icon: theme.icon ?? null,
577
+ label: theme.label
578
+ };
579
+ }, [agentId, agentThemes]);
580
+ }
581
+
582
+ // src/hooks/useCharacterDrain.ts
583
+ var import_react6 = require("react");
584
+ function useCharacterDrain(target, msPerChar = 15) {
585
+ const [displayed, setDisplayed] = (0, import_react6.useState)("");
586
+ const indexRef = (0, import_react6.useRef)(0);
587
+ const lastTimeRef = (0, import_react6.useRef)(0);
588
+ const rafRef = (0, import_react6.useRef)(null);
589
+ const drainTargetRef = (0, import_react6.useRef)("");
590
+ const msPerCharRef = (0, import_react6.useRef)(msPerChar);
591
+ msPerCharRef.current = msPerChar;
592
+ if (target !== "") {
593
+ drainTargetRef.current = target;
594
+ }
595
+ const drainTarget = drainTargetRef.current;
596
+ const isDraining = displayed.length < drainTarget.length;
597
+ const tickRef = (0, import_react6.useRef)(() => {
598
+ });
599
+ tickRef.current = (now) => {
600
+ const currentTarget = drainTargetRef.current;
601
+ if (currentTarget === "") {
602
+ rafRef.current = null;
603
+ return;
604
+ }
605
+ if (lastTimeRef.current === 0) lastTimeRef.current = now;
606
+ const elapsed = now - lastTimeRef.current;
607
+ const charsToAdvance = Math.floor(elapsed / msPerCharRef.current);
608
+ if (charsToAdvance > 0 && indexRef.current < currentTarget.length) {
609
+ const nextIndex = Math.min(indexRef.current + charsToAdvance, currentTarget.length);
610
+ indexRef.current = nextIndex;
611
+ lastTimeRef.current = now;
612
+ setDisplayed(currentTarget.slice(0, nextIndex));
613
+ }
614
+ if (indexRef.current < currentTarget.length) {
615
+ rafRef.current = requestAnimationFrame((t) => tickRef.current(t));
616
+ } else {
617
+ rafRef.current = null;
618
+ }
619
+ };
620
+ (0, import_react6.useEffect)(() => {
621
+ if (drainTargetRef.current !== "" && indexRef.current < drainTargetRef.current.length && rafRef.current === null) {
622
+ rafRef.current = requestAnimationFrame((t) => tickRef.current(t));
623
+ }
624
+ }, [drainTarget]);
625
+ (0, import_react6.useEffect)(() => {
626
+ if (target === "" && !isDraining && displayed !== "") {
627
+ indexRef.current = 0;
628
+ lastTimeRef.current = 0;
629
+ drainTargetRef.current = "";
630
+ setDisplayed("");
631
+ }
632
+ }, [target, isDraining, displayed]);
633
+ (0, import_react6.useEffect)(() => {
634
+ return () => {
635
+ if (rafRef.current !== null) {
636
+ cancelAnimationFrame(rafRef.current);
637
+ rafRef.current = null;
638
+ }
639
+ };
640
+ }, []);
641
+ return { displayed, isDraining };
642
+ }
643
+ // Annotate the CommonJS export names for ESM import in node:
644
+ 0 && (module.exports = {
645
+ useAgentChat,
646
+ useAgentTheme,
647
+ useCharacterDrain,
648
+ useConversation,
649
+ useFeedback,
650
+ useStreaming
651
+ });
652
+ //# sourceMappingURL=hooks.cjs.map