@cstar.help/react 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.
@@ -0,0 +1,218 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { ChatClient, ChatConfig, IdentifyParams, IdentifyResult, Conversation, ConversationCreateParams, WidgetMessage } from '@cstar.help/js/chat';
4
+ export { ChatConfig, Conversation, ConversationCreateParams, ConversationListResponse, IdentifyParams, IdentifyResult, TypingPayload, WidgetMessage } from '@cstar.help/js/chat';
5
+ import { LibraryClient, LibraryConfig, Category, LibraryArticle, LibraryArticleListParams } from '@cstar.help/js/library';
6
+ export { Category, LibraryArticle, LibraryArticleListParams, LibraryConfig, LibrarySearchParams, LibrarySearchResult } from '@cstar.help/js/library';
7
+ import * as react_jsx_runtime from 'react/jsx-runtime';
8
+
9
+ declare const CStarChatContext: react.Context<ChatClient | null>;
10
+ declare const CStarLibraryContext: react.Context<LibraryClient | null>;
11
+ /**
12
+ * Access the ChatClient from the nearest CStarChatProvider.
13
+ * Throws if used outside a provider.
14
+ */
15
+ declare function useChatClient(): ChatClient;
16
+ /**
17
+ * Access the LibraryClient from the nearest CStarLibraryProvider.
18
+ * Throws if used outside a provider.
19
+ */
20
+ declare function useLibraryClient(): LibraryClient;
21
+
22
+ interface CStarChatProviderProps extends ChatConfig {
23
+ children: ReactNode;
24
+ }
25
+ /**
26
+ * Provides a ChatClient instance to all child components.
27
+ * Creates the client on mount and disconnects on unmount.
28
+ *
29
+ * ```tsx
30
+ * <CStarChatProvider teamSlug="acme" supabaseUrl="..." supabaseAnonKey="...">
31
+ * <ChatWidget />
32
+ * </CStarChatProvider>
33
+ * ```
34
+ */
35
+ declare function CStarChatProvider({ children, ...config }: CStarChatProviderProps): react_jsx_runtime.JSX.Element;
36
+
37
+ interface UseChatReturn {
38
+ /** Identify a customer with HMAC signature. */
39
+ identify: (customer: IdentifyParams, signature: string) => Promise<IdentifyResult>;
40
+ /** Disconnect the client and clean up all subscriptions. */
41
+ disconnect: () => void;
42
+ /** Whether a customer has been successfully identified. */
43
+ isIdentified: boolean;
44
+ /** Whether the Supabase Realtime connection is ready. */
45
+ isRealtimeReady: boolean;
46
+ /** The last error from identify, or null. */
47
+ error: Error | null;
48
+ }
49
+ /**
50
+ * Core chat hook — manages identity verification and realtime readiness.
51
+ *
52
+ * ```tsx
53
+ * const { identify, disconnect, isIdentified, isRealtimeReady, error } = useChat();
54
+ *
55
+ * await identify(
56
+ * { externalId: 'usr_123', email: 'jane@co.com', timestamp: Date.now() / 1000 },
57
+ * hmacSignature
58
+ * );
59
+ * ```
60
+ */
61
+ declare function useChat(): UseChatReturn;
62
+
63
+ interface UseConversationsReturn {
64
+ /** List of conversations for the identified customer. */
65
+ conversations: Conversation[];
66
+ /** Whether the initial fetch is in progress. */
67
+ isLoading: boolean;
68
+ /** The last error, or null. */
69
+ error: Error | null;
70
+ /** Whether there are more conversations to load. */
71
+ hasMore: boolean;
72
+ /** Re-fetch the conversation list. */
73
+ refresh: () => Promise<void>;
74
+ /** Create a new conversation. */
75
+ create: (params: ConversationCreateParams) => Promise<Conversation>;
76
+ }
77
+ /**
78
+ * Manage the customer's conversation list.
79
+ *
80
+ * ```tsx
81
+ * const { conversations, isLoading, create, refresh } = useConversations();
82
+ * ```
83
+ */
84
+ declare function useConversations(): UseConversationsReturn;
85
+
86
+ interface UseMessagesReturn {
87
+ /** Messages in the conversation, oldest first. */
88
+ messages: WidgetMessage[];
89
+ /** Whether the initial fetch is in progress. */
90
+ isLoading: boolean;
91
+ /** The last error, or null. */
92
+ error: Error | null;
93
+ /** Send a message. Optimistically appended, replaced with server response. */
94
+ send: (content: string) => Promise<WidgetMessage>;
95
+ /** Re-fetch messages from the API. */
96
+ refresh: () => Promise<void>;
97
+ }
98
+ /**
99
+ * Manage messages for a single conversation with automatic real-time updates.
100
+ *
101
+ * ```tsx
102
+ * const { messages, isLoading, send } = useMessages(ticketId);
103
+ * await send('Hello!');
104
+ * ```
105
+ */
106
+ declare function useMessages(ticketId: string | null): UseMessagesReturn;
107
+
108
+ interface TypingAgent {
109
+ agentId: string;
110
+ agentName: string;
111
+ }
112
+ interface UseTypingReturn {
113
+ /** Agents currently typing. Auto-cleared after 4s of no activity. */
114
+ typingAgents: TypingAgent[];
115
+ /** Send a customer typing indicator. */
116
+ sendTyping: (isTyping: boolean) => Promise<void>;
117
+ }
118
+ /**
119
+ * Track agent typing indicators for a conversation.
120
+ *
121
+ * ```tsx
122
+ * const { typingAgents, sendTyping } = useTyping(ticketId);
123
+ * ```
124
+ */
125
+ declare function useTyping(ticketId: string | null): UseTypingReturn;
126
+
127
+ interface CStarLibraryProviderProps extends LibraryConfig {
128
+ children: ReactNode;
129
+ }
130
+ /**
131
+ * Provides a LibraryClient instance to all child components.
132
+ *
133
+ * ```tsx
134
+ * <CStarLibraryProvider teamSlug="acme">
135
+ * <KnowledgeBase />
136
+ * </CStarLibraryProvider>
137
+ * ```
138
+ */
139
+ declare function CStarLibraryProvider({ children, ...config }: CStarLibraryProviderProps): react_jsx_runtime.JSX.Element;
140
+
141
+ interface UseCategoriesReturn {
142
+ /** All public categories. */
143
+ categories: Category[];
144
+ /** Whether the initial fetch is in progress. */
145
+ isLoading: boolean;
146
+ /** The last error, or null. */
147
+ error: Error | null;
148
+ /** Re-fetch categories. */
149
+ refresh: () => Promise<void>;
150
+ }
151
+ /**
152
+ * Fetch public knowledge base categories.
153
+ *
154
+ * ```tsx
155
+ * const { categories, isLoading, error } = useCategories();
156
+ * ```
157
+ */
158
+ declare function useCategories(): UseCategoriesReturn;
159
+
160
+ interface UseArticlesReturn {
161
+ /** Published articles. */
162
+ articles: LibraryArticle[];
163
+ /** Whether the fetch is in progress. */
164
+ isLoading: boolean;
165
+ /** The last error, or null. */
166
+ error: Error | null;
167
+ /** Re-fetch articles with the current params. */
168
+ refresh: () => Promise<void>;
169
+ }
170
+ /**
171
+ * Fetch published articles, optionally filtered by category.
172
+ *
173
+ * ```tsx
174
+ * const { articles, isLoading } = useArticles({ categorySlug: 'getting-started' });
175
+ * ```
176
+ */
177
+ declare function useArticles(params?: LibraryArticleListParams): UseArticlesReturn;
178
+
179
+ interface UseArticleReturn {
180
+ /** The article, or null if not yet loaded. */
181
+ article: LibraryArticle | null;
182
+ /** Whether the fetch is in progress. */
183
+ isLoading: boolean;
184
+ /** The last error, or null. */
185
+ error: Error | null;
186
+ /** Re-fetch the article. */
187
+ refresh: () => Promise<void>;
188
+ }
189
+ /**
190
+ * Fetch a single article by slug.
191
+ *
192
+ * ```tsx
193
+ * const { article, isLoading, error } = useArticle('getting-started');
194
+ * ```
195
+ */
196
+ declare function useArticle(slug: string | null): UseArticleReturn;
197
+
198
+ interface UseArticleSearchReturn {
199
+ /** Search results. */
200
+ results: LibraryArticle[];
201
+ /** Total number of matching articles. */
202
+ totalCount: number;
203
+ /** Whether a search is in progress. */
204
+ isLoading: boolean;
205
+ /** The last error, or null. */
206
+ error: Error | null;
207
+ }
208
+ /**
209
+ * Search articles with automatic debouncing (300ms).
210
+ * Clears results when query is empty.
211
+ *
212
+ * ```tsx
213
+ * const { results, totalCount, isLoading } = useArticleSearch(searchTerm);
214
+ * ```
215
+ */
216
+ declare function useArticleSearch(query: string): UseArticleSearchReturn;
217
+
218
+ export { CStarChatContext, CStarChatProvider, type CStarChatProviderProps, CStarLibraryContext, CStarLibraryProvider, type CStarLibraryProviderProps, type TypingAgent, type UseArticleReturn, type UseArticleSearchReturn, type UseArticlesReturn, type UseCategoriesReturn, type UseChatReturn, type UseConversationsReturn, type UseMessagesReturn, type UseTypingReturn, useArticle, useArticleSearch, useArticles, useCategories, useChat, useChatClient, useConversations, useLibraryClient, useMessages, useTyping };
package/dist/index.js ADDED
@@ -0,0 +1,454 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext } from "react";
3
+ var CStarChatContext = createContext(null);
4
+ var CStarLibraryContext = createContext(null);
5
+ function useChatClient() {
6
+ const client = useContext(CStarChatContext);
7
+ if (!client) {
8
+ throw new Error("useChatClient must be used within a <CStarChatProvider>");
9
+ }
10
+ return client;
11
+ }
12
+ function useLibraryClient() {
13
+ const client = useContext(CStarLibraryContext);
14
+ if (!client) {
15
+ throw new Error("useLibraryClient must be used within a <CStarLibraryProvider>");
16
+ }
17
+ return client;
18
+ }
19
+
20
+ // src/chat/CStarChatProvider.tsx
21
+ import { useEffect, useRef } from "react";
22
+ import { ChatClient } from "@cstar.help/js/chat";
23
+ import { jsx } from "react/jsx-runtime";
24
+ function CStarChatProvider({ children, ...config }) {
25
+ const clientRef = useRef(null);
26
+ if (!clientRef.current) {
27
+ clientRef.current = new ChatClient(config);
28
+ }
29
+ useEffect(() => {
30
+ return () => {
31
+ clientRef.current?.disconnect();
32
+ clientRef.current = null;
33
+ };
34
+ }, []);
35
+ return /* @__PURE__ */ jsx(CStarChatContext.Provider, { value: clientRef.current, children });
36
+ }
37
+
38
+ // src/chat/useChat.ts
39
+ import { useState, useCallback } from "react";
40
+ function useChat() {
41
+ const client = useChatClient();
42
+ const [isIdentified, setIsIdentified] = useState(false);
43
+ const [isRealtimeReady, setIsRealtimeReady] = useState(false);
44
+ const [error, setError] = useState(null);
45
+ const identify = useCallback(
46
+ async (customer, signature) => {
47
+ setError(null);
48
+ try {
49
+ const result = await client.identify(customer, signature);
50
+ setIsIdentified(true);
51
+ client.waitForRealtime().then(() => {
52
+ setIsRealtimeReady(true);
53
+ }).catch(() => {
54
+ });
55
+ return result;
56
+ } catch (err) {
57
+ const e = err instanceof Error ? err : new Error(String(err));
58
+ setError(e);
59
+ throw e;
60
+ }
61
+ },
62
+ [client]
63
+ );
64
+ const disconnect = useCallback(() => {
65
+ client.disconnect();
66
+ setIsIdentified(false);
67
+ setIsRealtimeReady(false);
68
+ setError(null);
69
+ }, [client]);
70
+ return { identify, disconnect, isIdentified, isRealtimeReady, error };
71
+ }
72
+
73
+ // src/chat/useConversations.ts
74
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2 } from "react";
75
+ function useConversations() {
76
+ const client = useChatClient();
77
+ const [conversations, setConversations] = useState2([]);
78
+ const [isLoading, setIsLoading] = useState2(true);
79
+ const [error, setError] = useState2(null);
80
+ const [hasMore, setHasMore] = useState2(false);
81
+ const mountedRef = useRef2(true);
82
+ const refresh = useCallback2(async () => {
83
+ setIsLoading(true);
84
+ setError(null);
85
+ try {
86
+ const result = await client.conversations.list();
87
+ if (mountedRef.current) {
88
+ setConversations(result.conversations);
89
+ setHasMore(result.hasMore);
90
+ }
91
+ } catch (err) {
92
+ if (mountedRef.current) {
93
+ setError(err instanceof Error ? err : new Error(String(err)));
94
+ }
95
+ } finally {
96
+ if (mountedRef.current) {
97
+ setIsLoading(false);
98
+ }
99
+ }
100
+ }, [client]);
101
+ useEffect2(() => {
102
+ mountedRef.current = true;
103
+ refresh();
104
+ return () => {
105
+ mountedRef.current = false;
106
+ };
107
+ }, [refresh]);
108
+ const create = useCallback2(
109
+ async (params) => {
110
+ const conversation = await client.conversations.create(params);
111
+ if (mountedRef.current) {
112
+ setConversations((prev) => [conversation, ...prev]);
113
+ }
114
+ return conversation;
115
+ },
116
+ [client]
117
+ );
118
+ return { conversations, isLoading, error, hasMore, refresh, create };
119
+ }
120
+
121
+ // src/chat/useMessages.ts
122
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef as useRef3 } from "react";
123
+ function useMessages(ticketId) {
124
+ const client = useChatClient();
125
+ const [messages, setMessages] = useState3([]);
126
+ const [isLoading, setIsLoading] = useState3(true);
127
+ const [error, setError] = useState3(null);
128
+ const mountedRef = useRef3(true);
129
+ const seenIdsRef = useRef3(/* @__PURE__ */ new Set());
130
+ const refresh = useCallback3(async () => {
131
+ if (!ticketId) return;
132
+ setIsLoading(true);
133
+ setError(null);
134
+ try {
135
+ const msgs = await client.messages.list(ticketId);
136
+ if (mountedRef.current) {
137
+ seenIdsRef.current = new Set(msgs.map((m) => m.id));
138
+ setMessages(msgs);
139
+ }
140
+ } catch (err) {
141
+ if (mountedRef.current) {
142
+ setError(err instanceof Error ? err : new Error(String(err)));
143
+ }
144
+ } finally {
145
+ if (mountedRef.current) {
146
+ setIsLoading(false);
147
+ }
148
+ }
149
+ }, [client, ticketId]);
150
+ useEffect3(() => {
151
+ mountedRef.current = true;
152
+ if (!ticketId) {
153
+ setMessages([]);
154
+ setIsLoading(false);
155
+ return;
156
+ }
157
+ refresh();
158
+ const unsubscribe = client.onMessage(ticketId, (msg) => {
159
+ if (mountedRef.current && !seenIdsRef.current.has(msg.id)) {
160
+ seenIdsRef.current.add(msg.id);
161
+ setMessages((prev) => {
162
+ const optimisticIdx = prev.findIndex(
163
+ (m) => m.id.startsWith("optimistic-") && m.content === msg.content
164
+ );
165
+ if (optimisticIdx >= 0) {
166
+ const next = [...prev];
167
+ next[optimisticIdx] = msg;
168
+ return next;
169
+ }
170
+ return [...prev, msg];
171
+ });
172
+ }
173
+ });
174
+ return () => {
175
+ mountedRef.current = false;
176
+ unsubscribe();
177
+ };
178
+ }, [client, ticketId, refresh]);
179
+ const send = useCallback3(
180
+ async (content) => {
181
+ if (!ticketId) throw new Error("No conversation selected");
182
+ const optimistic = {
183
+ id: `optimistic-${Date.now()}`,
184
+ content,
185
+ sender: "customer",
186
+ sender_name: "",
187
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
188
+ read_at: null,
189
+ attachments: []
190
+ };
191
+ setMessages((prev) => [...prev, optimistic]);
192
+ try {
193
+ const msg = await client.messages.send(ticketId, content);
194
+ seenIdsRef.current.add(msg.id);
195
+ setMessages(
196
+ (prev) => prev.map((m) => m.id === optimistic.id ? msg : m)
197
+ );
198
+ return msg;
199
+ } catch (err) {
200
+ setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
201
+ throw err;
202
+ }
203
+ },
204
+ [client, ticketId]
205
+ );
206
+ return { messages, isLoading, error, send, refresh };
207
+ }
208
+
209
+ // src/chat/useTyping.ts
210
+ import { useState as useState4, useEffect as useEffect4, useCallback as useCallback4, useRef as useRef4 } from "react";
211
+ var TYPING_TIMEOUT = 4e3;
212
+ function useTyping(ticketId) {
213
+ const client = useChatClient();
214
+ const [typingAgents, setTypingAgents] = useState4([]);
215
+ const timersRef = useRef4(/* @__PURE__ */ new Map());
216
+ const mountedRef = useRef4(true);
217
+ useEffect4(() => {
218
+ mountedRef.current = true;
219
+ if (!ticketId) {
220
+ setTypingAgents([]);
221
+ return;
222
+ }
223
+ const unsubscribe = client.onTyping(ticketId, (payload) => {
224
+ if (!mountedRef.current) return;
225
+ const { agent_id, agent_name, is_typing } = payload;
226
+ const existing = timersRef.current.get(agent_id);
227
+ if (existing) clearTimeout(existing);
228
+ if (is_typing) {
229
+ setTypingAgents((prev) => {
230
+ if (prev.some((a) => a.agentId === agent_id)) return prev;
231
+ return [...prev, { agentId: agent_id, agentName: agent_name }];
232
+ });
233
+ const timer = setTimeout(() => {
234
+ if (mountedRef.current) {
235
+ setTypingAgents((prev) => prev.filter((a) => a.agentId !== agent_id));
236
+ timersRef.current.delete(agent_id);
237
+ }
238
+ }, TYPING_TIMEOUT);
239
+ timersRef.current.set(agent_id, timer);
240
+ } else {
241
+ setTypingAgents((prev) => prev.filter((a) => a.agentId !== agent_id));
242
+ timersRef.current.delete(agent_id);
243
+ }
244
+ });
245
+ return () => {
246
+ mountedRef.current = false;
247
+ unsubscribe();
248
+ for (const timer of timersRef.current.values()) {
249
+ clearTimeout(timer);
250
+ }
251
+ timersRef.current.clear();
252
+ };
253
+ }, [client, ticketId]);
254
+ const sendTyping = useCallback4(
255
+ async (isTyping) => {
256
+ if (!ticketId) return;
257
+ await client.sendTyping(ticketId, isTyping);
258
+ },
259
+ [client, ticketId]
260
+ );
261
+ return { typingAgents, sendTyping };
262
+ }
263
+
264
+ // src/library/CStarLibraryProvider.tsx
265
+ import { useRef as useRef5 } from "react";
266
+ import { LibraryClient } from "@cstar.help/js/library";
267
+ import { jsx as jsx2 } from "react/jsx-runtime";
268
+ function CStarLibraryProvider({ children, ...config }) {
269
+ const clientRef = useRef5(null);
270
+ if (!clientRef.current) {
271
+ clientRef.current = new LibraryClient(config);
272
+ }
273
+ return /* @__PURE__ */ jsx2(CStarLibraryContext.Provider, { value: clientRef.current, children });
274
+ }
275
+
276
+ // src/library/useCategories.ts
277
+ import { useState as useState5, useEffect as useEffect5, useCallback as useCallback5, useRef as useRef6 } from "react";
278
+ function useCategories() {
279
+ const client = useLibraryClient();
280
+ const [categories, setCategories] = useState5([]);
281
+ const [isLoading, setIsLoading] = useState5(true);
282
+ const [error, setError] = useState5(null);
283
+ const mountedRef = useRef6(true);
284
+ const refresh = useCallback5(async () => {
285
+ setIsLoading(true);
286
+ setError(null);
287
+ try {
288
+ const data = await client.categories();
289
+ if (mountedRef.current) {
290
+ setCategories(data);
291
+ }
292
+ } catch (err) {
293
+ if (mountedRef.current) {
294
+ setError(err instanceof Error ? err : new Error(String(err)));
295
+ }
296
+ } finally {
297
+ if (mountedRef.current) {
298
+ setIsLoading(false);
299
+ }
300
+ }
301
+ }, [client]);
302
+ useEffect5(() => {
303
+ mountedRef.current = true;
304
+ refresh();
305
+ return () => {
306
+ mountedRef.current = false;
307
+ };
308
+ }, [refresh]);
309
+ return { categories, isLoading, error, refresh };
310
+ }
311
+
312
+ // src/library/useArticles.ts
313
+ import { useState as useState6, useEffect as useEffect6, useCallback as useCallback6, useRef as useRef7 } from "react";
314
+ function useArticles(params) {
315
+ const client = useLibraryClient();
316
+ const [articles, setArticles] = useState6([]);
317
+ const [isLoading, setIsLoading] = useState6(true);
318
+ const [error, setError] = useState6(null);
319
+ const mountedRef = useRef7(true);
320
+ const categorySlug = params?.categorySlug;
321
+ const limit = params?.limit;
322
+ const offset = params?.offset;
323
+ const refresh = useCallback6(async () => {
324
+ setIsLoading(true);
325
+ setError(null);
326
+ try {
327
+ const data = await client.articles({ categorySlug, limit, offset });
328
+ if (mountedRef.current) {
329
+ setArticles(data);
330
+ }
331
+ } catch (err) {
332
+ if (mountedRef.current) {
333
+ setError(err instanceof Error ? err : new Error(String(err)));
334
+ }
335
+ } finally {
336
+ if (mountedRef.current) {
337
+ setIsLoading(false);
338
+ }
339
+ }
340
+ }, [client, categorySlug, limit, offset]);
341
+ useEffect6(() => {
342
+ mountedRef.current = true;
343
+ refresh();
344
+ return () => {
345
+ mountedRef.current = false;
346
+ };
347
+ }, [refresh]);
348
+ return { articles, isLoading, error, refresh };
349
+ }
350
+
351
+ // src/library/useArticle.ts
352
+ import { useState as useState7, useEffect as useEffect7, useCallback as useCallback7, useRef as useRef8 } from "react";
353
+ function useArticle(slug) {
354
+ const client = useLibraryClient();
355
+ const [article, setArticle] = useState7(null);
356
+ const [isLoading, setIsLoading] = useState7(true);
357
+ const [error, setError] = useState7(null);
358
+ const mountedRef = useRef8(true);
359
+ const refresh = useCallback7(async () => {
360
+ if (!slug) return;
361
+ setIsLoading(true);
362
+ setError(null);
363
+ try {
364
+ const data = await client.article(slug);
365
+ if (mountedRef.current) {
366
+ setArticle(data);
367
+ }
368
+ } catch (err) {
369
+ if (mountedRef.current) {
370
+ setError(err instanceof Error ? err : new Error(String(err)));
371
+ }
372
+ } finally {
373
+ if (mountedRef.current) {
374
+ setIsLoading(false);
375
+ }
376
+ }
377
+ }, [client, slug]);
378
+ useEffect7(() => {
379
+ mountedRef.current = true;
380
+ if (!slug) {
381
+ setArticle(null);
382
+ setIsLoading(false);
383
+ return;
384
+ }
385
+ refresh();
386
+ return () => {
387
+ mountedRef.current = false;
388
+ };
389
+ }, [slug, refresh]);
390
+ return { article, isLoading, error, refresh };
391
+ }
392
+
393
+ // src/library/useArticleSearch.ts
394
+ import { useState as useState8, useEffect as useEffect8, useRef as useRef9 } from "react";
395
+ var DEBOUNCE_MS = 300;
396
+ function useArticleSearch(query) {
397
+ const client = useLibraryClient();
398
+ const [results, setResults] = useState8([]);
399
+ const [totalCount, setTotalCount] = useState8(0);
400
+ const [isLoading, setIsLoading] = useState8(false);
401
+ const [error, setError] = useState8(null);
402
+ const mountedRef = useRef9(true);
403
+ useEffect8(() => {
404
+ mountedRef.current = true;
405
+ const trimmed = query.trim();
406
+ if (!trimmed) {
407
+ setResults([]);
408
+ setTotalCount(0);
409
+ setIsLoading(false);
410
+ setError(null);
411
+ return;
412
+ }
413
+ setIsLoading(true);
414
+ const timer = setTimeout(async () => {
415
+ try {
416
+ const result = await client.search(trimmed);
417
+ if (mountedRef.current) {
418
+ setResults(result.data);
419
+ setTotalCount(result.count);
420
+ setError(null);
421
+ }
422
+ } catch (err) {
423
+ if (mountedRef.current) {
424
+ setError(err instanceof Error ? err : new Error(String(err)));
425
+ }
426
+ } finally {
427
+ if (mountedRef.current) {
428
+ setIsLoading(false);
429
+ }
430
+ }
431
+ }, DEBOUNCE_MS);
432
+ return () => {
433
+ mountedRef.current = false;
434
+ clearTimeout(timer);
435
+ };
436
+ }, [client, query]);
437
+ return { results, totalCount, isLoading, error };
438
+ }
439
+ export {
440
+ CStarChatContext,
441
+ CStarChatProvider,
442
+ CStarLibraryContext,
443
+ CStarLibraryProvider,
444
+ useArticle,
445
+ useArticleSearch,
446
+ useArticles,
447
+ useCategories,
448
+ useChat,
449
+ useChatClient,
450
+ useConversations,
451
+ useLibraryClient,
452
+ useMessages,
453
+ useTyping
454
+ };