@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.
- package/dist/index.cjs +494 -0
- package/dist/index.d.cts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +454 -0
- package/package.json +63 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
};
|