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