@tigmart/ai-react 0.0.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.
@@ -0,0 +1,65 @@
1
+ import { Message } from '@topsoft/ai-types';
2
+
3
+ interface UseChatOptions {
4
+ providerId: string;
5
+ modelId: string;
6
+ apiUrl?: string;
7
+ }
8
+ interface UseChatReturn {
9
+ messages: Message[];
10
+ sendMessage: (text: string) => Promise<void>;
11
+ cancel: () => void;
12
+ isLoading: boolean;
13
+ error: string | null;
14
+ }
15
+ declare function useChat({ providerId, modelId, apiUrl, }: UseChatOptions): UseChatReturn;
16
+
17
+ type ProviderId = 'openai' | 'claude' | 'gemini';
18
+ declare const AVAILABLE_PROVIDERS: {
19
+ id: ProviderId;
20
+ label: string;
21
+ }[];
22
+ interface Project {
23
+ id: string;
24
+ name: string;
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }
28
+ interface ChatThread {
29
+ id: string;
30
+ title: string;
31
+ messages: Message[];
32
+ projectId?: string | null;
33
+ createdAt: number;
34
+ updatedAt: number;
35
+ }
36
+ interface UseMultiChatOptions {
37
+ apiUrl?: string;
38
+ }
39
+ interface UseMultiChatReturn {
40
+ threads: ChatThread[];
41
+ currentThreadId: string;
42
+ messages: Message[];
43
+ sendMessage: (text: string) => Promise<void>;
44
+ cancel: () => void;
45
+ isLoading: boolean;
46
+ error: string | null;
47
+ provider: ProviderId;
48
+ setProvider: (provider: ProviderId) => void;
49
+ comparisonMode: boolean;
50
+ setComparisonMode: (enabled: boolean) => void;
51
+ selectedProviders: ProviderId[];
52
+ setSelectedProviders: (providers: ProviderId[]) => void;
53
+ createNewChat: () => void;
54
+ selectChat: (id: string) => void;
55
+ renameChat: (threadId: string, newTitle: string) => void;
56
+ deleteChat: (threadId: string) => void;
57
+ markAsBest: (messageId: string) => void;
58
+ projects: Project[];
59
+ createProject: (name: string) => void;
60
+ assignThreadToProject: (threadId: string, projectId: string | null) => void;
61
+ }
62
+
63
+ declare function useMultiChat({ apiUrl, }?: UseMultiChatOptions): UseMultiChatReturn;
64
+
65
+ export { AVAILABLE_PROVIDERS, type ChatThread, type Project, type ProviderId, type UseChatOptions, type UseChatReturn, type UseMultiChatOptions, type UseMultiChatReturn, useChat, useMultiChat };
@@ -0,0 +1,65 @@
1
+ import { Message } from '@topsoft/ai-types';
2
+
3
+ interface UseChatOptions {
4
+ providerId: string;
5
+ modelId: string;
6
+ apiUrl?: string;
7
+ }
8
+ interface UseChatReturn {
9
+ messages: Message[];
10
+ sendMessage: (text: string) => Promise<void>;
11
+ cancel: () => void;
12
+ isLoading: boolean;
13
+ error: string | null;
14
+ }
15
+ declare function useChat({ providerId, modelId, apiUrl, }: UseChatOptions): UseChatReturn;
16
+
17
+ type ProviderId = 'openai' | 'claude' | 'gemini';
18
+ declare const AVAILABLE_PROVIDERS: {
19
+ id: ProviderId;
20
+ label: string;
21
+ }[];
22
+ interface Project {
23
+ id: string;
24
+ name: string;
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }
28
+ interface ChatThread {
29
+ id: string;
30
+ title: string;
31
+ messages: Message[];
32
+ projectId?: string | null;
33
+ createdAt: number;
34
+ updatedAt: number;
35
+ }
36
+ interface UseMultiChatOptions {
37
+ apiUrl?: string;
38
+ }
39
+ interface UseMultiChatReturn {
40
+ threads: ChatThread[];
41
+ currentThreadId: string;
42
+ messages: Message[];
43
+ sendMessage: (text: string) => Promise<void>;
44
+ cancel: () => void;
45
+ isLoading: boolean;
46
+ error: string | null;
47
+ provider: ProviderId;
48
+ setProvider: (provider: ProviderId) => void;
49
+ comparisonMode: boolean;
50
+ setComparisonMode: (enabled: boolean) => void;
51
+ selectedProviders: ProviderId[];
52
+ setSelectedProviders: (providers: ProviderId[]) => void;
53
+ createNewChat: () => void;
54
+ selectChat: (id: string) => void;
55
+ renameChat: (threadId: string, newTitle: string) => void;
56
+ deleteChat: (threadId: string) => void;
57
+ markAsBest: (messageId: string) => void;
58
+ projects: Project[];
59
+ createProject: (name: string) => void;
60
+ assignThreadToProject: (threadId: string, projectId: string | null) => void;
61
+ }
62
+
63
+ declare function useMultiChat({ apiUrl, }?: UseMultiChatOptions): UseMultiChatReturn;
64
+
65
+ export { AVAILABLE_PROVIDERS, type ChatThread, type Project, type ProviderId, type UseChatOptions, type UseChatReturn, type UseMultiChatOptions, type UseMultiChatReturn, useChat, useMultiChat };
package/dist/index.js ADDED
@@ -0,0 +1,564 @@
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/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AVAILABLE_PROVIDERS: () => AVAILABLE_PROVIDERS,
24
+ useChat: () => useChat,
25
+ useMultiChat: () => useMultiChat
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/hooks/useChat.ts
30
+ var import_react = require("react");
31
+ function makeMessage(conversationId, role, text, status) {
32
+ return {
33
+ id: crypto.randomUUID(),
34
+ conversationId,
35
+ role,
36
+ parts: [{ type: "text", text }],
37
+ status,
38
+ createdAt: Date.now(),
39
+ updatedAt: Date.now(),
40
+ metadata: {}
41
+ };
42
+ }
43
+ function useChat({
44
+ providerId,
45
+ modelId,
46
+ apiUrl = "/api/chat"
47
+ }) {
48
+ const [messages, setMessages] = (0, import_react.useState)([]);
49
+ const [isLoading, setIsLoading] = (0, import_react.useState)(false);
50
+ const [error, setError] = (0, import_react.useState)(null);
51
+ const [conversationId] = (0, import_react.useState)(() => crypto.randomUUID());
52
+ const abortRef = (0, import_react.useRef)(null);
53
+ const isMountedRef = (0, import_react.useRef)(true);
54
+ const messagesRef = (0, import_react.useRef)(messages);
55
+ (0, import_react.useEffect)(() => {
56
+ messagesRef.current = messages;
57
+ }, [messages]);
58
+ (0, import_react.useEffect)(() => {
59
+ return () => {
60
+ isMountedRef.current = false;
61
+ abortRef.current?.abort();
62
+ };
63
+ }, []);
64
+ const cancel = (0, import_react.useCallback)(() => {
65
+ abortRef.current?.abort();
66
+ }, []);
67
+ const sendMessage = (0, import_react.useCallback)(
68
+ async (text) => {
69
+ const trimmed = text.trim();
70
+ if (!trimmed || isLoading) return;
71
+ abortRef.current?.abort();
72
+ const abort = new AbortController();
73
+ abortRef.current = abort;
74
+ setError(null);
75
+ setIsLoading(true);
76
+ const userMessage = makeMessage(conversationId, "user", trimmed, "done");
77
+ const assistantMessage = makeMessage(conversationId, "assistant", "", "streaming");
78
+ const messagesForApi = [...messagesRef.current, userMessage];
79
+ setMessages((prev) => [...prev, userMessage, assistantMessage]);
80
+ try {
81
+ const response = await fetch(apiUrl, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ messages: messagesForApi, modelId, providerId }),
85
+ signal: abort.signal
86
+ });
87
+ if (!response.ok) {
88
+ let errorMsg = `HTTP ${response.status}`;
89
+ try {
90
+ const json = await response.json();
91
+ if (typeof json.error === "string") errorMsg = json.error;
92
+ } catch {
93
+ }
94
+ throw new Error(errorMsg);
95
+ }
96
+ if (!response.body) {
97
+ throw new Error("Response body is empty");
98
+ }
99
+ const reader = response.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ let lineBuffer = "";
102
+ let finalStatus = "done";
103
+ let streamError;
104
+ const processLine = (raw) => {
105
+ const trimmedLine = raw.trim();
106
+ if (!trimmedLine) return false;
107
+ let chunk;
108
+ try {
109
+ chunk = JSON.parse(trimmedLine);
110
+ } catch {
111
+ return false;
112
+ }
113
+ if (chunk.type === "delta" && typeof chunk.content === "string") {
114
+ const delta = chunk.content;
115
+ setMessages((prev) => {
116
+ const next = [...prev];
117
+ const last = next[next.length - 1];
118
+ if (!last || last.role !== "assistant") return next;
119
+ const existing = last.parts[0]?.type === "text" ? last.parts[0].text : "";
120
+ next[next.length - 1] = {
121
+ ...last,
122
+ parts: [{ type: "text", text: existing + delta }],
123
+ updatedAt: Date.now()
124
+ };
125
+ return next;
126
+ });
127
+ return false;
128
+ } else if (chunk.type === "error") {
129
+ finalStatus = "error";
130
+ streamError = chunk.error ?? "Streaming error";
131
+ return true;
132
+ } else if (chunk.type === "done") {
133
+ return true;
134
+ }
135
+ return false;
136
+ };
137
+ outer: while (true) {
138
+ const { done, value } = await reader.read();
139
+ if (done) {
140
+ lineBuffer += decoder.decode();
141
+ if (lineBuffer.trim()) processLine(lineBuffer);
142
+ break;
143
+ }
144
+ lineBuffer += decoder.decode(value, { stream: true });
145
+ const lines = lineBuffer.split("\n");
146
+ lineBuffer = lines.pop() ?? "";
147
+ for (const line of lines) {
148
+ if (processLine(line)) break outer;
149
+ }
150
+ }
151
+ if (!isMountedRef.current) return;
152
+ if (streamError) setError(streamError);
153
+ setMessages((prev) => {
154
+ const next = [...prev];
155
+ const last = next[next.length - 1];
156
+ if (!last || last.role !== "assistant") return next;
157
+ next[next.length - 1] = {
158
+ ...last,
159
+ status: finalStatus,
160
+ ...streamError !== void 0 ? { error: streamError } : {},
161
+ updatedAt: Date.now()
162
+ };
163
+ return next;
164
+ });
165
+ } catch (err) {
166
+ if (abort.signal.aborted || !isMountedRef.current) return;
167
+ const message = err instanceof Error ? err.message : "Unknown error";
168
+ setError(message);
169
+ setMessages((prev) => {
170
+ const next = [...prev];
171
+ const last = next[next.length - 1];
172
+ if (!last || last.role !== "assistant") return next;
173
+ next[next.length - 1] = {
174
+ ...last,
175
+ status: "error",
176
+ error: message,
177
+ updatedAt: Date.now()
178
+ };
179
+ return next;
180
+ });
181
+ } finally {
182
+ setIsLoading(false);
183
+ }
184
+ },
185
+ [isLoading, conversationId, apiUrl, modelId, providerId]
186
+ );
187
+ return { messages, sendMessage, cancel, isLoading, error };
188
+ }
189
+
190
+ // src/hooks/useMultiChat.ts
191
+ var import_react2 = require("react");
192
+
193
+ // src/types/index.ts
194
+ var AVAILABLE_PROVIDERS = [
195
+ { id: "openai", label: "OpenAI" },
196
+ { id: "gemini", label: "Gemini" },
197
+ { id: "claude", label: "Claude" }
198
+ ];
199
+
200
+ // src/hooks/useMultiChat.ts
201
+ var MODEL_BY_PROVIDER = {
202
+ openai: "gpt-4o-mini",
203
+ claude: "claude-3-5-sonnet-20241022",
204
+ gemini: "gemini-2.5-flash"
205
+ };
206
+ var STORAGE_KEY = "ai-chat-playground";
207
+ function isValidThread(val) {
208
+ if (!val || typeof val !== "object") return false;
209
+ const t = val;
210
+ return typeof t["id"] === "string" && typeof t["title"] === "string" && Array.isArray(t["messages"]) && typeof t["createdAt"] === "number" && typeof t["updatedAt"] === "number";
211
+ }
212
+ function isValidProject(val) {
213
+ if (!val || typeof val !== "object") return false;
214
+ const p = val;
215
+ return typeof p["id"] === "string" && typeof p["name"] === "string" && typeof p["createdAt"] === "number" && typeof p["updatedAt"] === "number";
216
+ }
217
+ function loadFromStorage() {
218
+ try {
219
+ const raw = localStorage.getItem(STORAGE_KEY);
220
+ if (!raw) return null;
221
+ const parsed = JSON.parse(raw);
222
+ if (!parsed || typeof parsed !== "object") return null;
223
+ const data = parsed;
224
+ if (!Array.isArray(data["threads"])) return null;
225
+ const threads = data["threads"].filter(isValidThread);
226
+ if (threads.length === 0) return null;
227
+ const storedId = typeof data["currentThreadId"] === "string" ? data["currentThreadId"] : "";
228
+ const currentThreadId = threads.some((t) => t.id === storedId) ? storedId : threads[0]?.id ?? "";
229
+ const projects = Array.isArray(data["projects"]) ? data["projects"].filter(isValidProject) : [];
230
+ return { threads, currentThreadId, projects };
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ function saveToStorage(state) {
236
+ try {
237
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
238
+ } catch {
239
+ }
240
+ }
241
+ function makeMessage2(conversationId, role, text, status) {
242
+ return {
243
+ id: crypto.randomUUID(),
244
+ conversationId,
245
+ role,
246
+ parts: [{ type: "text", text }],
247
+ status,
248
+ createdAt: Date.now(),
249
+ updatedAt: Date.now(),
250
+ metadata: {}
251
+ };
252
+ }
253
+ function createThread() {
254
+ return {
255
+ id: crypto.randomUUID(),
256
+ title: "New chat",
257
+ messages: [],
258
+ createdAt: Date.now(),
259
+ updatedAt: Date.now()
260
+ };
261
+ }
262
+ function useMultiChat({
263
+ apiUrl = "/api/chat"
264
+ } = {}) {
265
+ const defaultThread = (0, import_react2.useRef)(createThread());
266
+ const [{ threads, currentThreadId, projects }, setChatState] = (0, import_react2.useState)(() => ({
267
+ threads: [defaultThread.current],
268
+ currentThreadId: defaultThread.current.id,
269
+ projects: []
270
+ }));
271
+ const [isMounted, setIsMounted] = (0, import_react2.useState)(false);
272
+ const [isLoading, setIsLoading] = (0, import_react2.useState)(false);
273
+ const [error, setError] = (0, import_react2.useState)(null);
274
+ const [provider, setProvider] = (0, import_react2.useState)("openai");
275
+ const [comparisonMode, setComparisonMode] = (0, import_react2.useState)(false);
276
+ const [selectedProviders, setSelectedProviders] = (0, import_react2.useState)(["openai", "gemini"]);
277
+ const abortRef = (0, import_react2.useRef)(null);
278
+ const isMountedRef = (0, import_react2.useRef)(true);
279
+ const threadsRef = (0, import_react2.useRef)(threads);
280
+ (0, import_react2.useEffect)(() => {
281
+ threadsRef.current = threads;
282
+ }, [threads]);
283
+ (0, import_react2.useEffect)(() => {
284
+ return () => {
285
+ isMountedRef.current = false;
286
+ abortRef.current?.abort();
287
+ };
288
+ }, []);
289
+ (0, import_react2.useEffect)(() => {
290
+ setIsMounted(true);
291
+ const stored = loadFromStorage();
292
+ if (stored) {
293
+ setChatState(stored);
294
+ }
295
+ }, []);
296
+ (0, import_react2.useEffect)(() => {
297
+ if (!isMounted) return;
298
+ saveToStorage({ threads, currentThreadId, projects });
299
+ }, [isMounted, threads, currentThreadId, projects]);
300
+ const currentThread = threads.find((t) => t.id === currentThreadId) ?? threads[0] ?? {
301
+ id: currentThreadId,
302
+ title: "New chat",
303
+ messages: [],
304
+ createdAt: Date.now(),
305
+ updatedAt: Date.now()
306
+ };
307
+ const messages = currentThread.messages;
308
+ const createNewChat = (0, import_react2.useCallback)(() => {
309
+ const thread = createThread();
310
+ setChatState((prev) => ({
311
+ ...prev,
312
+ threads: [...prev.threads, thread],
313
+ currentThreadId: thread.id
314
+ }));
315
+ setError(null);
316
+ }, []);
317
+ const selectChat = (0, import_react2.useCallback)((id) => {
318
+ setChatState((prev) => ({ ...prev, currentThreadId: id }));
319
+ setError(null);
320
+ }, []);
321
+ const renameChat = (0, import_react2.useCallback)((id, newTitle) => {
322
+ const trimmed = newTitle.trim();
323
+ if (!trimmed) return;
324
+ setChatState((prev) => ({
325
+ ...prev,
326
+ threads: prev.threads.map(
327
+ (t) => t.id === id ? { ...t, title: trimmed, updatedAt: Date.now() } : t
328
+ )
329
+ }));
330
+ }, []);
331
+ const deleteChat = (0, import_react2.useCallback)((id) => {
332
+ setChatState((prev) => {
333
+ const remaining = prev.threads.filter((t) => t.id !== id);
334
+ const next = remaining.length > 0 ? remaining : [createThread()];
335
+ const currentThreadId2 = prev.currentThreadId === id ? next[0]?.id ?? prev.currentThreadId : prev.currentThreadId;
336
+ return { ...prev, threads: next, currentThreadId: currentThreadId2 };
337
+ });
338
+ setError(null);
339
+ }, []);
340
+ const cancel = (0, import_react2.useCallback)(() => {
341
+ abortRef.current?.abort();
342
+ }, []);
343
+ const createProject = (0, import_react2.useCallback)((name) => {
344
+ const trimmed = name.trim();
345
+ if (!trimmed) return;
346
+ const project = {
347
+ id: crypto.randomUUID(),
348
+ name: trimmed,
349
+ createdAt: Date.now(),
350
+ updatedAt: Date.now()
351
+ };
352
+ setChatState((prev) => ({ ...prev, projects: [...prev.projects, project] }));
353
+ }, []);
354
+ const assignThreadToProject = (0, import_react2.useCallback)((threadId, projectId) => {
355
+ setChatState((prev) => ({
356
+ ...prev,
357
+ threads: prev.threads.map(
358
+ (t) => t.id === threadId ? { ...t, projectId, updatedAt: Date.now() } : t
359
+ )
360
+ }));
361
+ }, []);
362
+ const markAsBest = (0, import_react2.useCallback)(
363
+ (messageId) => {
364
+ setChatState((prev) => ({
365
+ ...prev,
366
+ threads: prev.threads.map((t) => {
367
+ if (t.id !== currentThreadId) return t;
368
+ const target = t.messages.find((m) => m.id === messageId);
369
+ if (!target) return t;
370
+ const parentId = target.metadata.parentId;
371
+ return {
372
+ ...t,
373
+ messages: t.messages.map((m) => {
374
+ if (m.role !== "assistant" || m.metadata.parentId !== parentId) return m;
375
+ return { ...m, metadata: { ...m.metadata, isBest: m.id === messageId } };
376
+ })
377
+ };
378
+ })
379
+ }));
380
+ },
381
+ [currentThreadId]
382
+ );
383
+ const sendMessage = (0, import_react2.useCallback)(
384
+ async (text) => {
385
+ const trimmed = text.trim();
386
+ if (!trimmed || isLoading) return;
387
+ const threadId = currentThreadId;
388
+ const capturedProvider = provider;
389
+ const capturedComparisonMode = comparisonMode;
390
+ const capturedSelectedProviders = selectedProviders;
391
+ abortRef.current?.abort();
392
+ const abort = new AbortController();
393
+ abortRef.current = abort;
394
+ setError(null);
395
+ setIsLoading(true);
396
+ const thread = threadsRef.current.find((t) => t.id === threadId);
397
+ const isFirstMessage = !thread?.messages.length;
398
+ const userMessage = makeMessage2(threadId, "user", trimmed, "done");
399
+ const history = [...thread?.messages ?? [], userMessage];
400
+ const effectiveProviders = capturedComparisonMode && capturedSelectedProviders.length > 0 ? capturedSelectedProviders : capturedComparisonMode ? ["openai"] : [capturedProvider];
401
+ const assistantMessages = effectiveProviders.map((p) => ({
402
+ ...makeMessage2(threadId, "assistant", "", "streaming"),
403
+ providerId: p,
404
+ metadata: { parentId: userMessage.id }
405
+ }));
406
+ setChatState((prev) => ({
407
+ ...prev,
408
+ threads: prev.threads.map((t) => {
409
+ if (t.id !== threadId) return t;
410
+ return {
411
+ ...t,
412
+ title: isFirstMessage ? trimmed.slice(0, 40) : t.title,
413
+ messages: [...t.messages, userMessage, ...assistantMessages],
414
+ updatedAt: Date.now()
415
+ };
416
+ })
417
+ }));
418
+ async function streamIntoMessage(p, msgId) {
419
+ const modelId = MODEL_BY_PROVIDER[p];
420
+ const startedAt = Date.now();
421
+ function updateById(updater) {
422
+ setChatState((prev) => ({
423
+ ...prev,
424
+ threads: prev.threads.map((t) => {
425
+ if (t.id !== threadId) return t;
426
+ return {
427
+ ...t,
428
+ messages: t.messages.map((m) => m.id === msgId ? updater(m) : m),
429
+ updatedAt: Date.now()
430
+ };
431
+ })
432
+ }));
433
+ }
434
+ let finalStatus = "done";
435
+ let streamError;
436
+ function processLine(raw) {
437
+ const line = raw.trim();
438
+ if (!line) return false;
439
+ let chunk;
440
+ try {
441
+ chunk = JSON.parse(line);
442
+ } catch {
443
+ return false;
444
+ }
445
+ if (chunk.type === "delta" && typeof chunk.content === "string") {
446
+ const delta = chunk.content;
447
+ updateById((msg) => {
448
+ const existing = msg.parts[0]?.type === "text" ? msg.parts[0].text : "";
449
+ return {
450
+ ...msg,
451
+ parts: [{ type: "text", text: existing + delta }],
452
+ updatedAt: Date.now()
453
+ };
454
+ });
455
+ return false;
456
+ } else if (chunk.type === "error") {
457
+ finalStatus = "error";
458
+ streamError = chunk.error ?? "Streaming error";
459
+ return true;
460
+ } else if (chunk.type === "done") {
461
+ return true;
462
+ }
463
+ return false;
464
+ }
465
+ try {
466
+ const response = await fetch(apiUrl, {
467
+ method: "POST",
468
+ headers: { "Content-Type": "application/json" },
469
+ body: JSON.stringify({ messages: history, modelId, providerId: p }),
470
+ signal: abort.signal
471
+ });
472
+ if (!response.ok) {
473
+ let errorMsg = `HTTP ${response.status}`;
474
+ try {
475
+ const json = await response.json();
476
+ if (typeof json.error === "string") errorMsg = json.error;
477
+ } catch {
478
+ }
479
+ throw new Error(errorMsg);
480
+ }
481
+ if (!response.body) throw new Error("Response body is empty");
482
+ const reader = response.body.getReader();
483
+ const decoder = new TextDecoder();
484
+ let lineBuffer = "";
485
+ outer: while (true) {
486
+ const { done, value } = await reader.read();
487
+ if (done) {
488
+ lineBuffer += decoder.decode();
489
+ if (lineBuffer.trim()) processLine(lineBuffer);
490
+ break;
491
+ }
492
+ lineBuffer += decoder.decode(value, { stream: true });
493
+ const lines = lineBuffer.split("\n");
494
+ lineBuffer = lines.pop() ?? "";
495
+ for (const ln of lines) {
496
+ if (processLine(ln)) break outer;
497
+ }
498
+ }
499
+ if (!isMountedRef.current) return;
500
+ if (streamError) setError(streamError);
501
+ const completedAt = Date.now();
502
+ updateById((msg) => ({
503
+ ...msg,
504
+ status: finalStatus,
505
+ ...streamError !== void 0 ? { error: streamError } : {},
506
+ updatedAt: completedAt,
507
+ metadata: { ...msg.metadata, startedAt, completedAt, durationMs: completedAt - startedAt }
508
+ }));
509
+ } catch (err) {
510
+ if (abort.signal.aborted || !isMountedRef.current) return;
511
+ const message = err instanceof Error ? err.message : "Unknown error";
512
+ const completedAt = Date.now();
513
+ setError(message);
514
+ updateById((msg) => ({
515
+ ...msg,
516
+ status: "error",
517
+ error: message,
518
+ updatedAt: completedAt,
519
+ metadata: { ...msg.metadata, startedAt, completedAt, durationMs: completedAt - startedAt }
520
+ }));
521
+ }
522
+ }
523
+ try {
524
+ await Promise.allSettled(
525
+ effectiveProviders.map(
526
+ (p, i) => streamIntoMessage(p, assistantMessages[i].id)
527
+ )
528
+ );
529
+ } finally {
530
+ setIsLoading(false);
531
+ }
532
+ },
533
+ [isLoading, currentThreadId, apiUrl, provider, comparisonMode, selectedProviders]
534
+ );
535
+ return {
536
+ threads,
537
+ currentThreadId,
538
+ messages,
539
+ sendMessage,
540
+ cancel,
541
+ isLoading,
542
+ error,
543
+ provider,
544
+ setProvider,
545
+ comparisonMode,
546
+ setComparisonMode,
547
+ selectedProviders,
548
+ setSelectedProviders,
549
+ createNewChat,
550
+ selectChat,
551
+ renameChat,
552
+ deleteChat,
553
+ markAsBest,
554
+ projects,
555
+ createProject,
556
+ assignThreadToProject
557
+ };
558
+ }
559
+ // Annotate the CommonJS export names for ESM import in node:
560
+ 0 && (module.exports = {
561
+ AVAILABLE_PROVIDERS,
562
+ useChat,
563
+ useMultiChat
564
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,535 @@
1
+ // src/hooks/useChat.ts
2
+ import { useState, useCallback, useRef, useEffect } from "react";
3
+ function makeMessage(conversationId, role, text, status) {
4
+ return {
5
+ id: crypto.randomUUID(),
6
+ conversationId,
7
+ role,
8
+ parts: [{ type: "text", text }],
9
+ status,
10
+ createdAt: Date.now(),
11
+ updatedAt: Date.now(),
12
+ metadata: {}
13
+ };
14
+ }
15
+ function useChat({
16
+ providerId,
17
+ modelId,
18
+ apiUrl = "/api/chat"
19
+ }) {
20
+ const [messages, setMessages] = useState([]);
21
+ const [isLoading, setIsLoading] = useState(false);
22
+ const [error, setError] = useState(null);
23
+ const [conversationId] = useState(() => crypto.randomUUID());
24
+ const abortRef = useRef(null);
25
+ const isMountedRef = useRef(true);
26
+ const messagesRef = useRef(messages);
27
+ useEffect(() => {
28
+ messagesRef.current = messages;
29
+ }, [messages]);
30
+ useEffect(() => {
31
+ return () => {
32
+ isMountedRef.current = false;
33
+ abortRef.current?.abort();
34
+ };
35
+ }, []);
36
+ const cancel = useCallback(() => {
37
+ abortRef.current?.abort();
38
+ }, []);
39
+ const sendMessage = useCallback(
40
+ async (text) => {
41
+ const trimmed = text.trim();
42
+ if (!trimmed || isLoading) return;
43
+ abortRef.current?.abort();
44
+ const abort = new AbortController();
45
+ abortRef.current = abort;
46
+ setError(null);
47
+ setIsLoading(true);
48
+ const userMessage = makeMessage(conversationId, "user", trimmed, "done");
49
+ const assistantMessage = makeMessage(conversationId, "assistant", "", "streaming");
50
+ const messagesForApi = [...messagesRef.current, userMessage];
51
+ setMessages((prev) => [...prev, userMessage, assistantMessage]);
52
+ try {
53
+ const response = await fetch(apiUrl, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({ messages: messagesForApi, modelId, providerId }),
57
+ signal: abort.signal
58
+ });
59
+ if (!response.ok) {
60
+ let errorMsg = `HTTP ${response.status}`;
61
+ try {
62
+ const json = await response.json();
63
+ if (typeof json.error === "string") errorMsg = json.error;
64
+ } catch {
65
+ }
66
+ throw new Error(errorMsg);
67
+ }
68
+ if (!response.body) {
69
+ throw new Error("Response body is empty");
70
+ }
71
+ const reader = response.body.getReader();
72
+ const decoder = new TextDecoder();
73
+ let lineBuffer = "";
74
+ let finalStatus = "done";
75
+ let streamError;
76
+ const processLine = (raw) => {
77
+ const trimmedLine = raw.trim();
78
+ if (!trimmedLine) return false;
79
+ let chunk;
80
+ try {
81
+ chunk = JSON.parse(trimmedLine);
82
+ } catch {
83
+ return false;
84
+ }
85
+ if (chunk.type === "delta" && typeof chunk.content === "string") {
86
+ const delta = chunk.content;
87
+ setMessages((prev) => {
88
+ const next = [...prev];
89
+ const last = next[next.length - 1];
90
+ if (!last || last.role !== "assistant") return next;
91
+ const existing = last.parts[0]?.type === "text" ? last.parts[0].text : "";
92
+ next[next.length - 1] = {
93
+ ...last,
94
+ parts: [{ type: "text", text: existing + delta }],
95
+ updatedAt: Date.now()
96
+ };
97
+ return next;
98
+ });
99
+ return false;
100
+ } else if (chunk.type === "error") {
101
+ finalStatus = "error";
102
+ streamError = chunk.error ?? "Streaming error";
103
+ return true;
104
+ } else if (chunk.type === "done") {
105
+ return true;
106
+ }
107
+ return false;
108
+ };
109
+ outer: while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done) {
112
+ lineBuffer += decoder.decode();
113
+ if (lineBuffer.trim()) processLine(lineBuffer);
114
+ break;
115
+ }
116
+ lineBuffer += decoder.decode(value, { stream: true });
117
+ const lines = lineBuffer.split("\n");
118
+ lineBuffer = lines.pop() ?? "";
119
+ for (const line of lines) {
120
+ if (processLine(line)) break outer;
121
+ }
122
+ }
123
+ if (!isMountedRef.current) return;
124
+ if (streamError) setError(streamError);
125
+ setMessages((prev) => {
126
+ const next = [...prev];
127
+ const last = next[next.length - 1];
128
+ if (!last || last.role !== "assistant") return next;
129
+ next[next.length - 1] = {
130
+ ...last,
131
+ status: finalStatus,
132
+ ...streamError !== void 0 ? { error: streamError } : {},
133
+ updatedAt: Date.now()
134
+ };
135
+ return next;
136
+ });
137
+ } catch (err) {
138
+ if (abort.signal.aborted || !isMountedRef.current) return;
139
+ const message = err instanceof Error ? err.message : "Unknown error";
140
+ setError(message);
141
+ setMessages((prev) => {
142
+ const next = [...prev];
143
+ const last = next[next.length - 1];
144
+ if (!last || last.role !== "assistant") return next;
145
+ next[next.length - 1] = {
146
+ ...last,
147
+ status: "error",
148
+ error: message,
149
+ updatedAt: Date.now()
150
+ };
151
+ return next;
152
+ });
153
+ } finally {
154
+ setIsLoading(false);
155
+ }
156
+ },
157
+ [isLoading, conversationId, apiUrl, modelId, providerId]
158
+ );
159
+ return { messages, sendMessage, cancel, isLoading, error };
160
+ }
161
+
162
+ // src/hooks/useMultiChat.ts
163
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
164
+
165
+ // src/types/index.ts
166
+ var AVAILABLE_PROVIDERS = [
167
+ { id: "openai", label: "OpenAI" },
168
+ { id: "gemini", label: "Gemini" },
169
+ { id: "claude", label: "Claude" }
170
+ ];
171
+
172
+ // src/hooks/useMultiChat.ts
173
+ var MODEL_BY_PROVIDER = {
174
+ openai: "gpt-4o-mini",
175
+ claude: "claude-3-5-sonnet-20241022",
176
+ gemini: "gemini-2.5-flash"
177
+ };
178
+ var STORAGE_KEY = "ai-chat-playground";
179
+ function isValidThread(val) {
180
+ if (!val || typeof val !== "object") return false;
181
+ const t = val;
182
+ return typeof t["id"] === "string" && typeof t["title"] === "string" && Array.isArray(t["messages"]) && typeof t["createdAt"] === "number" && typeof t["updatedAt"] === "number";
183
+ }
184
+ function isValidProject(val) {
185
+ if (!val || typeof val !== "object") return false;
186
+ const p = val;
187
+ return typeof p["id"] === "string" && typeof p["name"] === "string" && typeof p["createdAt"] === "number" && typeof p["updatedAt"] === "number";
188
+ }
189
+ function loadFromStorage() {
190
+ try {
191
+ const raw = localStorage.getItem(STORAGE_KEY);
192
+ if (!raw) return null;
193
+ const parsed = JSON.parse(raw);
194
+ if (!parsed || typeof parsed !== "object") return null;
195
+ const data = parsed;
196
+ if (!Array.isArray(data["threads"])) return null;
197
+ const threads = data["threads"].filter(isValidThread);
198
+ if (threads.length === 0) return null;
199
+ const storedId = typeof data["currentThreadId"] === "string" ? data["currentThreadId"] : "";
200
+ const currentThreadId = threads.some((t) => t.id === storedId) ? storedId : threads[0]?.id ?? "";
201
+ const projects = Array.isArray(data["projects"]) ? data["projects"].filter(isValidProject) : [];
202
+ return { threads, currentThreadId, projects };
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+ function saveToStorage(state) {
208
+ try {
209
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
210
+ } catch {
211
+ }
212
+ }
213
+ function makeMessage2(conversationId, role, text, status) {
214
+ return {
215
+ id: crypto.randomUUID(),
216
+ conversationId,
217
+ role,
218
+ parts: [{ type: "text", text }],
219
+ status,
220
+ createdAt: Date.now(),
221
+ updatedAt: Date.now(),
222
+ metadata: {}
223
+ };
224
+ }
225
+ function createThread() {
226
+ return {
227
+ id: crypto.randomUUID(),
228
+ title: "New chat",
229
+ messages: [],
230
+ createdAt: Date.now(),
231
+ updatedAt: Date.now()
232
+ };
233
+ }
234
+ function useMultiChat({
235
+ apiUrl = "/api/chat"
236
+ } = {}) {
237
+ const defaultThread = useRef2(createThread());
238
+ const [{ threads, currentThreadId, projects }, setChatState] = useState2(() => ({
239
+ threads: [defaultThread.current],
240
+ currentThreadId: defaultThread.current.id,
241
+ projects: []
242
+ }));
243
+ const [isMounted, setIsMounted] = useState2(false);
244
+ const [isLoading, setIsLoading] = useState2(false);
245
+ const [error, setError] = useState2(null);
246
+ const [provider, setProvider] = useState2("openai");
247
+ const [comparisonMode, setComparisonMode] = useState2(false);
248
+ const [selectedProviders, setSelectedProviders] = useState2(["openai", "gemini"]);
249
+ const abortRef = useRef2(null);
250
+ const isMountedRef = useRef2(true);
251
+ const threadsRef = useRef2(threads);
252
+ useEffect2(() => {
253
+ threadsRef.current = threads;
254
+ }, [threads]);
255
+ useEffect2(() => {
256
+ return () => {
257
+ isMountedRef.current = false;
258
+ abortRef.current?.abort();
259
+ };
260
+ }, []);
261
+ useEffect2(() => {
262
+ setIsMounted(true);
263
+ const stored = loadFromStorage();
264
+ if (stored) {
265
+ setChatState(stored);
266
+ }
267
+ }, []);
268
+ useEffect2(() => {
269
+ if (!isMounted) return;
270
+ saveToStorage({ threads, currentThreadId, projects });
271
+ }, [isMounted, threads, currentThreadId, projects]);
272
+ const currentThread = threads.find((t) => t.id === currentThreadId) ?? threads[0] ?? {
273
+ id: currentThreadId,
274
+ title: "New chat",
275
+ messages: [],
276
+ createdAt: Date.now(),
277
+ updatedAt: Date.now()
278
+ };
279
+ const messages = currentThread.messages;
280
+ const createNewChat = useCallback2(() => {
281
+ const thread = createThread();
282
+ setChatState((prev) => ({
283
+ ...prev,
284
+ threads: [...prev.threads, thread],
285
+ currentThreadId: thread.id
286
+ }));
287
+ setError(null);
288
+ }, []);
289
+ const selectChat = useCallback2((id) => {
290
+ setChatState((prev) => ({ ...prev, currentThreadId: id }));
291
+ setError(null);
292
+ }, []);
293
+ const renameChat = useCallback2((id, newTitle) => {
294
+ const trimmed = newTitle.trim();
295
+ if (!trimmed) return;
296
+ setChatState((prev) => ({
297
+ ...prev,
298
+ threads: prev.threads.map(
299
+ (t) => t.id === id ? { ...t, title: trimmed, updatedAt: Date.now() } : t
300
+ )
301
+ }));
302
+ }, []);
303
+ const deleteChat = useCallback2((id) => {
304
+ setChatState((prev) => {
305
+ const remaining = prev.threads.filter((t) => t.id !== id);
306
+ const next = remaining.length > 0 ? remaining : [createThread()];
307
+ const currentThreadId2 = prev.currentThreadId === id ? next[0]?.id ?? prev.currentThreadId : prev.currentThreadId;
308
+ return { ...prev, threads: next, currentThreadId: currentThreadId2 };
309
+ });
310
+ setError(null);
311
+ }, []);
312
+ const cancel = useCallback2(() => {
313
+ abortRef.current?.abort();
314
+ }, []);
315
+ const createProject = useCallback2((name) => {
316
+ const trimmed = name.trim();
317
+ if (!trimmed) return;
318
+ const project = {
319
+ id: crypto.randomUUID(),
320
+ name: trimmed,
321
+ createdAt: Date.now(),
322
+ updatedAt: Date.now()
323
+ };
324
+ setChatState((prev) => ({ ...prev, projects: [...prev.projects, project] }));
325
+ }, []);
326
+ const assignThreadToProject = useCallback2((threadId, projectId) => {
327
+ setChatState((prev) => ({
328
+ ...prev,
329
+ threads: prev.threads.map(
330
+ (t) => t.id === threadId ? { ...t, projectId, updatedAt: Date.now() } : t
331
+ )
332
+ }));
333
+ }, []);
334
+ const markAsBest = useCallback2(
335
+ (messageId) => {
336
+ setChatState((prev) => ({
337
+ ...prev,
338
+ threads: prev.threads.map((t) => {
339
+ if (t.id !== currentThreadId) return t;
340
+ const target = t.messages.find((m) => m.id === messageId);
341
+ if (!target) return t;
342
+ const parentId = target.metadata.parentId;
343
+ return {
344
+ ...t,
345
+ messages: t.messages.map((m) => {
346
+ if (m.role !== "assistant" || m.metadata.parentId !== parentId) return m;
347
+ return { ...m, metadata: { ...m.metadata, isBest: m.id === messageId } };
348
+ })
349
+ };
350
+ })
351
+ }));
352
+ },
353
+ [currentThreadId]
354
+ );
355
+ const sendMessage = useCallback2(
356
+ async (text) => {
357
+ const trimmed = text.trim();
358
+ if (!trimmed || isLoading) return;
359
+ const threadId = currentThreadId;
360
+ const capturedProvider = provider;
361
+ const capturedComparisonMode = comparisonMode;
362
+ const capturedSelectedProviders = selectedProviders;
363
+ abortRef.current?.abort();
364
+ const abort = new AbortController();
365
+ abortRef.current = abort;
366
+ setError(null);
367
+ setIsLoading(true);
368
+ const thread = threadsRef.current.find((t) => t.id === threadId);
369
+ const isFirstMessage = !thread?.messages.length;
370
+ const userMessage = makeMessage2(threadId, "user", trimmed, "done");
371
+ const history = [...thread?.messages ?? [], userMessage];
372
+ const effectiveProviders = capturedComparisonMode && capturedSelectedProviders.length > 0 ? capturedSelectedProviders : capturedComparisonMode ? ["openai"] : [capturedProvider];
373
+ const assistantMessages = effectiveProviders.map((p) => ({
374
+ ...makeMessage2(threadId, "assistant", "", "streaming"),
375
+ providerId: p,
376
+ metadata: { parentId: userMessage.id }
377
+ }));
378
+ setChatState((prev) => ({
379
+ ...prev,
380
+ threads: prev.threads.map((t) => {
381
+ if (t.id !== threadId) return t;
382
+ return {
383
+ ...t,
384
+ title: isFirstMessage ? trimmed.slice(0, 40) : t.title,
385
+ messages: [...t.messages, userMessage, ...assistantMessages],
386
+ updatedAt: Date.now()
387
+ };
388
+ })
389
+ }));
390
+ async function streamIntoMessage(p, msgId) {
391
+ const modelId = MODEL_BY_PROVIDER[p];
392
+ const startedAt = Date.now();
393
+ function updateById(updater) {
394
+ setChatState((prev) => ({
395
+ ...prev,
396
+ threads: prev.threads.map((t) => {
397
+ if (t.id !== threadId) return t;
398
+ return {
399
+ ...t,
400
+ messages: t.messages.map((m) => m.id === msgId ? updater(m) : m),
401
+ updatedAt: Date.now()
402
+ };
403
+ })
404
+ }));
405
+ }
406
+ let finalStatus = "done";
407
+ let streamError;
408
+ function processLine(raw) {
409
+ const line = raw.trim();
410
+ if (!line) return false;
411
+ let chunk;
412
+ try {
413
+ chunk = JSON.parse(line);
414
+ } catch {
415
+ return false;
416
+ }
417
+ if (chunk.type === "delta" && typeof chunk.content === "string") {
418
+ const delta = chunk.content;
419
+ updateById((msg) => {
420
+ const existing = msg.parts[0]?.type === "text" ? msg.parts[0].text : "";
421
+ return {
422
+ ...msg,
423
+ parts: [{ type: "text", text: existing + delta }],
424
+ updatedAt: Date.now()
425
+ };
426
+ });
427
+ return false;
428
+ } else if (chunk.type === "error") {
429
+ finalStatus = "error";
430
+ streamError = chunk.error ?? "Streaming error";
431
+ return true;
432
+ } else if (chunk.type === "done") {
433
+ return true;
434
+ }
435
+ return false;
436
+ }
437
+ try {
438
+ const response = await fetch(apiUrl, {
439
+ method: "POST",
440
+ headers: { "Content-Type": "application/json" },
441
+ body: JSON.stringify({ messages: history, modelId, providerId: p }),
442
+ signal: abort.signal
443
+ });
444
+ if (!response.ok) {
445
+ let errorMsg = `HTTP ${response.status}`;
446
+ try {
447
+ const json = await response.json();
448
+ if (typeof json.error === "string") errorMsg = json.error;
449
+ } catch {
450
+ }
451
+ throw new Error(errorMsg);
452
+ }
453
+ if (!response.body) throw new Error("Response body is empty");
454
+ const reader = response.body.getReader();
455
+ const decoder = new TextDecoder();
456
+ let lineBuffer = "";
457
+ outer: while (true) {
458
+ const { done, value } = await reader.read();
459
+ if (done) {
460
+ lineBuffer += decoder.decode();
461
+ if (lineBuffer.trim()) processLine(lineBuffer);
462
+ break;
463
+ }
464
+ lineBuffer += decoder.decode(value, { stream: true });
465
+ const lines = lineBuffer.split("\n");
466
+ lineBuffer = lines.pop() ?? "";
467
+ for (const ln of lines) {
468
+ if (processLine(ln)) break outer;
469
+ }
470
+ }
471
+ if (!isMountedRef.current) return;
472
+ if (streamError) setError(streamError);
473
+ const completedAt = Date.now();
474
+ updateById((msg) => ({
475
+ ...msg,
476
+ status: finalStatus,
477
+ ...streamError !== void 0 ? { error: streamError } : {},
478
+ updatedAt: completedAt,
479
+ metadata: { ...msg.metadata, startedAt, completedAt, durationMs: completedAt - startedAt }
480
+ }));
481
+ } catch (err) {
482
+ if (abort.signal.aborted || !isMountedRef.current) return;
483
+ const message = err instanceof Error ? err.message : "Unknown error";
484
+ const completedAt = Date.now();
485
+ setError(message);
486
+ updateById((msg) => ({
487
+ ...msg,
488
+ status: "error",
489
+ error: message,
490
+ updatedAt: completedAt,
491
+ metadata: { ...msg.metadata, startedAt, completedAt, durationMs: completedAt - startedAt }
492
+ }));
493
+ }
494
+ }
495
+ try {
496
+ await Promise.allSettled(
497
+ effectiveProviders.map(
498
+ (p, i) => streamIntoMessage(p, assistantMessages[i].id)
499
+ )
500
+ );
501
+ } finally {
502
+ setIsLoading(false);
503
+ }
504
+ },
505
+ [isLoading, currentThreadId, apiUrl, provider, comparisonMode, selectedProviders]
506
+ );
507
+ return {
508
+ threads,
509
+ currentThreadId,
510
+ messages,
511
+ sendMessage,
512
+ cancel,
513
+ isLoading,
514
+ error,
515
+ provider,
516
+ setProvider,
517
+ comparisonMode,
518
+ setComparisonMode,
519
+ selectedProviders,
520
+ setSelectedProviders,
521
+ createNewChat,
522
+ selectChat,
523
+ renameChat,
524
+ deleteChat,
525
+ markAsBest,
526
+ projects,
527
+ createProject,
528
+ assignThreadToProject
529
+ };
530
+ }
531
+ export {
532
+ AVAILABLE_PROVIDERS,
533
+ useChat,
534
+ useMultiChat
535
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@tigmart/ai-react",
3
+ "version": "0.0.1",
4
+ "description": "React hooks and state management for the AI chat platform",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "sideEffects": false,
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "dev": "tsup --watch",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@tigmart/ai-types": "workspace:*",
28
+ "zustand": "^5.0.0"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "^18.0.0 || ^19.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.0.0",
35
+ "react": "^19.0.0",
36
+ "tsup": "^8.3.0",
37
+ "typescript": "^5.7.0"
38
+ }
39
+ }