@melony/react 0.1.18 → 0.1.22

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.d.cts CHANGED
@@ -2,6 +2,7 @@ import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { ClientState, MelonyClient } from 'melony/client';
4
4
  import { Role, Event, UINode } from 'melony';
5
+ import { QueryClient } from '@tanstack/react-query';
5
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
7
 
7
8
  interface User {
@@ -63,12 +64,18 @@ interface MelonyContextValue extends ClientState {
63
64
  }) => Promise<void>;
64
65
  reset: (events?: Event[]) => void;
65
66
  client: MelonyClient;
67
+ config?: {
68
+ starterPrompts: any[];
69
+ options: any[];
70
+ };
66
71
  }
67
72
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
68
73
  interface MelonyClientProviderProps {
69
74
  children: ReactNode;
70
75
  client: MelonyClient;
71
76
  initialEvents?: Event[];
77
+ queryClient?: QueryClient;
78
+ configApi?: string;
72
79
  }
73
80
  declare const MelonyClientProvider: React__default.FC<MelonyClientProviderProps>;
74
81
 
@@ -136,7 +143,7 @@ interface ThreadProps {
136
143
  autoFocus?: boolean;
137
144
  defaultSelectedIds?: string[];
138
145
  }
139
- declare function Thread({ className, placeholder, starterPrompts, onStarterPromptClick, options, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
146
+ declare function Thread({ className, placeholder, starterPrompts: localStarterPrompts, onStarterPromptClick, options: localOptions, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
140
147
 
141
148
  interface ComposerProps {
142
149
  value: string;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import * as React$1 from 'react';
2
2
  import React__default, { ReactNode } from 'react';
3
3
  import { ClientState, MelonyClient } from 'melony/client';
4
4
  import { Role, Event, UINode } from 'melony';
5
+ import { QueryClient } from '@tanstack/react-query';
5
6
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
7
 
7
8
  interface User {
@@ -63,12 +64,18 @@ interface MelonyContextValue extends ClientState {
63
64
  }) => Promise<void>;
64
65
  reset: (events?: Event[]) => void;
65
66
  client: MelonyClient;
67
+ config?: {
68
+ starterPrompts: any[];
69
+ options: any[];
70
+ };
66
71
  }
67
72
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
68
73
  interface MelonyClientProviderProps {
69
74
  children: ReactNode;
70
75
  client: MelonyClient;
71
76
  initialEvents?: Event[];
77
+ queryClient?: QueryClient;
78
+ configApi?: string;
72
79
  }
73
80
  declare const MelonyClientProvider: React__default.FC<MelonyClientProviderProps>;
74
81
 
@@ -136,7 +143,7 @@ interface ThreadProps {
136
143
  autoFocus?: boolean;
137
144
  defaultSelectedIds?: string[];
138
145
  }
139
- declare function Thread({ className, placeholder, starterPrompts, onStarterPromptClick, options, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
146
+ declare function Thread({ className, placeholder, starterPrompts: localStarterPrompts, onStarterPromptClick, options: localOptions, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
140
147
 
141
148
  interface ComposerProps {
142
149
  value: string;
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import * as React10 from 'react';
2
- import React10__default, { createContext, useState, useEffect, useCallback, useMemo, useContext, useRef } from 'react';
2
+ import React10__default, { createContext, useState, useCallback, useEffect, useMemo, useContext, useRef } from 'react';
3
+ import { NuqsAdapter } from 'nuqs/adapters/react';
4
+ import { QueryClient, QueryClientProvider, useQueryClient, useQuery, useMutation } from '@tanstack/react-query';
3
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
6
  import { generateId } from 'melony/client';
7
+ import { useQueryState, parseAsString } from 'nuqs';
5
8
  import { clsx } from 'clsx';
6
9
  import { twMerge } from 'tailwind-merge';
7
10
  import { Button as Button$1 } from '@base-ui/react/button';
8
11
  import { cva } from 'class-variance-authority';
9
12
  import * as ICONS from '@tabler/icons-react';
10
- import { IconChevronDown, IconLoader2, IconArrowUp, IconPlus, IconMessage, IconTrash, IconHistory, IconX, IconArrowLeft, IconChevronLeft, IconChevronRight, IconUser, IconLogout, IconBrandGoogle, IconDeviceDesktop, IconMoon, IconSun, IconCheck, IconSelector, IconChevronUp } from '@tabler/icons-react';
13
+ import { IconChevronDown, IconLoader2, IconArrowUp, IconPlus, IconMessage, IconTrash, IconHistory, IconX, IconArrowLeft, IconChevronLeft, IconChevronRight, IconUser, IconLogout, IconBrandGoogle, IconDeviceDesktop, IconMoon, IconSun, IconCheck, IconChevronUp, IconSelector } from '@tabler/icons-react';
11
14
  import { Menu } from '@base-ui/react/menu';
12
15
  import { Separator as Separator$1 } from '@base-ui/react/separator';
13
16
  import { Dialog as Dialog$1 } from '@base-ui/react/dialog';
@@ -48,12 +51,28 @@ function groupEventsToMessages(events) {
48
51
  var MelonyContext = createContext(
49
52
  void 0
50
53
  );
51
- var MelonyClientProvider = ({
54
+ var defaultQueryClient = new QueryClient({
55
+ defaultOptions: {
56
+ queries: {
57
+ retry: false,
58
+ refetchOnWindowFocus: false
59
+ }
60
+ }
61
+ });
62
+ var MelonyContextProviderInner = ({
52
63
  children,
53
64
  client,
54
- initialEvents
65
+ initialEvents,
66
+ configApi,
67
+ setContextValue
55
68
  }) => {
56
69
  const [state, setState] = useState(client.getState());
70
+ const { data: config } = useQuery({
71
+ queryKey: ["melony-config", configApi],
72
+ queryFn: () => client.getConfig(configApi),
73
+ enabled: !!configApi,
74
+ staleTime: Infinity
75
+ });
57
76
  useEffect(() => {
58
77
  if (initialEvents && initialEvents.length > 0 && client.getState().events.length === 0) {
59
78
  client.reset(initialEvents);
@@ -84,11 +103,34 @@ var MelonyClientProvider = ({
84
103
  messages: groupEventsToMessages(state.events),
85
104
  sendEvent,
86
105
  reset,
87
- client
106
+ client,
107
+ config
88
108
  }),
89
- [state, sendEvent, reset, client]
109
+ [state, sendEvent, reset, client, config]
90
110
  );
91
- return /* @__PURE__ */ jsx(MelonyContext.Provider, { value, children });
111
+ useEffect(() => {
112
+ setContextValue(value);
113
+ }, [value, setContextValue]);
114
+ return /* @__PURE__ */ jsx(NuqsAdapter, { children });
115
+ };
116
+ var MelonyClientProvider = ({
117
+ children,
118
+ client,
119
+ initialEvents,
120
+ queryClient = defaultQueryClient,
121
+ configApi
122
+ }) => {
123
+ const [contextValue, setContextValue] = useState(void 0);
124
+ return /* @__PURE__ */ jsx(MelonyContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(
125
+ MelonyContextProviderInner,
126
+ {
127
+ client,
128
+ initialEvents,
129
+ configApi,
130
+ setContextValue,
131
+ children
132
+ }
133
+ ) }) });
92
134
  };
93
135
  var AuthContext = createContext(
94
136
  void 0
@@ -161,111 +203,75 @@ var ThreadProvider = ({
161
203
  service,
162
204
  initialThreadId: providedInitialThreadId
163
205
  }) => {
206
+ const queryClient = useQueryClient();
164
207
  const defaultInitialThreadId = useMemo(() => generateId(), []);
165
208
  const initialThreadId = providedInitialThreadId || defaultInitialThreadId;
166
- const [threads, setThreads] = useState([]);
167
- const [activeThreadId, setActiveThreadId] = useState(
168
- initialThreadId
209
+ const [activeThreadId, setActiveThreadId] = useQueryState(
210
+ "threadId",
211
+ parseAsString.withDefault(initialThreadId)
169
212
  );
170
- const [isLoading, setIsLoading] = useState(true);
171
- const [error, setError] = useState(null);
172
- const [threadEvents, setThreadEvents] = useState([]);
173
- const [isLoadingEvents, setIsLoadingEvents] = useState(false);
174
- const fetchThreads = useCallback(async () => {
175
- setIsLoading(true);
176
- setError(null);
177
- try {
178
- const processedThreads = await service.getThreads();
179
- setThreads(processedThreads);
180
- } catch (err) {
181
- const error2 = err instanceof Error ? err : new Error("Failed to fetch threads");
182
- setError(error2);
183
- console.error("Failed to fetch threads:", error2);
184
- } finally {
185
- setIsLoading(false);
213
+ const {
214
+ data: threads = [],
215
+ isLoading,
216
+ error: threadsError,
217
+ refetch: refreshThreads
218
+ } = useQuery({
219
+ queryKey: ["threads"],
220
+ queryFn: () => service.getThreads()
221
+ });
222
+ const { data: threadEvents = [], isLoading: isLoadingEvents } = useQuery({
223
+ queryKey: ["threads", activeThreadId, "events"],
224
+ queryFn: () => service.getEvents(activeThreadId),
225
+ enabled: !!activeThreadId
226
+ });
227
+ const createMutation = useMutation({
228
+ mutationFn: async () => {
229
+ const newId = service.createThread ? await service.createThread() : generateId();
230
+ return newId;
231
+ },
232
+ onSuccess: async (newId) => {
233
+ await queryClient.invalidateQueries({ queryKey: ["threads"] });
234
+ await setActiveThreadId(newId);
186
235
  }
187
- }, [service]);
188
- useEffect(() => {
189
- fetchThreads();
190
- }, [fetchThreads]);
191
- const selectThread = useCallback((threadId) => {
192
- setActiveThreadId(threadId);
193
- }, []);
236
+ });
237
+ const deleteMutation = useMutation({
238
+ mutationFn: (threadId) => service.deleteThread(threadId),
239
+ onSuccess: async (_, threadId) => {
240
+ await queryClient.invalidateQueries({ queryKey: ["threads"] });
241
+ if (activeThreadId === threadId) {
242
+ const remainingThreads = threads.filter((t) => t.id !== threadId);
243
+ const nextId = remainingThreads.length > 0 ? remainingThreads[0].id : null;
244
+ await setActiveThreadId(nextId);
245
+ }
246
+ }
247
+ });
248
+ const selectThread = useCallback(
249
+ (threadId) => {
250
+ setActiveThreadId(threadId);
251
+ },
252
+ [setActiveThreadId]
253
+ );
194
254
  const createThread = useCallback(async () => {
195
- const newId = service.createThread ? await service.createThread() : generateId();
196
- const newThread = {
197
- id: newId,
198
- updatedAt: /* @__PURE__ */ new Date()
199
- };
200
- setThreads((prev) => [newThread, ...prev]);
201
- setActiveThreadId(newId);
202
- return newId;
203
- }, [service]);
255
+ return createMutation.mutateAsync();
256
+ }, [createMutation]);
204
257
  const deleteThread = useCallback(
205
258
  async (threadId) => {
206
- try {
207
- await service.deleteThread(threadId);
208
- setThreads((prev) => {
209
- const remainingThreads = prev.filter((t) => t.id !== threadId);
210
- setActiveThreadId((current) => {
211
- if (current === threadId) {
212
- return remainingThreads.length > 0 ? remainingThreads[0].id : null;
213
- }
214
- return current;
215
- });
216
- return remainingThreads;
217
- });
218
- } catch (err) {
219
- const error2 = err instanceof Error ? err : new Error("Failed to delete thread");
220
- setError(error2);
221
- throw error2;
222
- }
259
+ return deleteMutation.mutateAsync(threadId);
223
260
  },
224
- [service]
261
+ [deleteMutation]
225
262
  );
226
- const refreshThreads = useCallback(async () => {
227
- await fetchThreads();
228
- }, [fetchThreads]);
229
- useEffect(() => {
230
- if (!activeThreadId) {
231
- setThreadEvents([]);
232
- setIsLoadingEvents(false);
233
- return;
234
- }
235
- let cancelled = false;
236
- const fetchEvents = async () => {
237
- setIsLoadingEvents(true);
238
- try {
239
- const events = await service.getEvents(activeThreadId);
240
- if (!cancelled) {
241
- setThreadEvents(events);
242
- }
243
- } catch (err) {
244
- if (!cancelled) {
245
- console.error("Failed to fetch events:", err);
246
- setThreadEvents([]);
247
- }
248
- } finally {
249
- if (!cancelled) {
250
- setIsLoadingEvents(false);
251
- }
252
- }
253
- };
254
- fetchEvents();
255
- return () => {
256
- cancelled = true;
257
- };
258
- }, [activeThreadId, service]);
259
263
  const value = useMemo(
260
264
  () => ({
261
265
  threads,
262
266
  activeThreadId,
263
267
  isLoading,
264
- error,
268
+ error: threadsError || null,
265
269
  selectThread,
266
270
  createThread,
267
271
  deleteThread,
268
- refreshThreads,
272
+ refreshThreads: async () => {
273
+ await refreshThreads();
274
+ },
269
275
  threadEvents,
270
276
  isLoadingEvents
271
277
  }),
@@ -273,7 +279,7 @@ var ThreadProvider = ({
273
279
  threads,
274
280
  activeThreadId,
275
281
  isLoading,
276
- error,
282
+ threadsError,
277
283
  selectThread,
278
284
  createThread,
279
285
  deleteThread,
@@ -343,9 +349,16 @@ var useMelony = (options) => {
343
349
  }
344
350
  const { client, reset } = context;
345
351
  const { initialEvents } = options || {};
352
+ const prevInitialEventsRef = useRef(void 0);
346
353
  useEffect(() => {
347
- if (initialEvents && initialEvents.length > 0 && client.getState().events.length === 0) {
348
- reset(initialEvents);
354
+ const currentSerialized = initialEvents ? JSON.stringify(initialEvents) : void 0;
355
+ if (currentSerialized !== prevInitialEventsRef.current) {
356
+ if (initialEvents) {
357
+ reset(initialEvents);
358
+ } else {
359
+ reset([]);
360
+ }
361
+ prevInitialEventsRef.current = currentSerialized;
349
362
  }
350
363
  }, [client, initialEvents, reset]);
351
364
  return context;
@@ -2316,13 +2329,18 @@ function MessageList({
2316
2329
  function Thread({
2317
2330
  className,
2318
2331
  placeholder = "Type a message...",
2319
- starterPrompts,
2332
+ starterPrompts: localStarterPrompts,
2320
2333
  onStarterPromptClick,
2321
- options,
2334
+ options: localOptions,
2322
2335
  autoFocus = false,
2323
2336
  defaultSelectedIds
2324
2337
  }) {
2325
- const { messages, isLoading, error, sendEvent, loadingStatus } = useMelony();
2338
+ const { activeThreadId, threadEvents, isLoadingEvents } = useThreads();
2339
+ const { messages, isLoading, error, sendEvent, loadingStatus, config } = useMelony({
2340
+ initialEvents: threadEvents
2341
+ });
2342
+ const starterPrompts = localStarterPrompts ?? config?.starterPrompts;
2343
+ const options = localOptions ?? config?.options;
2326
2344
  const [input, setInput] = useState("");
2327
2345
  const messagesEndRef = useRef(null);
2328
2346
  useEffect(() => {
@@ -2338,7 +2356,7 @@ function Thread({
2338
2356
  role: "user",
2339
2357
  data: { content: text }
2340
2358
  },
2341
- { state }
2359
+ { state: { ...state, threadId: activeThreadId ?? void 0 } }
2342
2360
  );
2343
2361
  };
2344
2362
  const handleStarterPromptClick = (prompt) => {
@@ -2348,21 +2366,21 @@ function Thread({
2348
2366
  handleSubmit(void 0, prompt);
2349
2367
  }
2350
2368
  };
2351
- const showStarterPrompts = messages.length === 0 && starterPrompts && starterPrompts.length > 0;
2369
+ const showStarterPrompts = messages.length === 0 && starterPrompts && starterPrompts.length > 0 && !isLoadingEvents;
2352
2370
  return /* @__PURE__ */ jsxs(
2353
2371
  "div",
2354
2372
  {
2355
2373
  className: cn("relative flex flex-col h-full bg-background", className),
2356
2374
  children: [
2357
2375
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 pb-36", children: [
2358
- /* @__PURE__ */ jsxs(
2376
+ /* @__PURE__ */ jsx(
2359
2377
  "div",
2360
2378
  {
2361
2379
  className: cn(
2362
2380
  "max-w-4xl mx-auto w-full p-4",
2363
2381
  showStarterPrompts && "min-h-full flex flex-col"
2364
2382
  ),
2365
- children: [
2383
+ children: isLoadingEvents && messages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-20", children: /* @__PURE__ */ jsx(LoadingIndicator, { status: { message: "Loading messages..." } }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2366
2384
  showStarterPrompts && /* @__PURE__ */ jsx(
2367
2385
  StarterPrompts,
2368
2386
  {
@@ -2379,7 +2397,7 @@ function Thread({
2379
2397
  loadingStatus
2380
2398
  }
2381
2399
  )
2382
- ]
2400
+ ] })
2383
2401
  }
2384
2402
  ),
2385
2403
  /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
@@ -2422,7 +2440,14 @@ var ThreadList = ({
2422
2440
  emptyState,
2423
2441
  onThreadSelect
2424
2442
  }) => {
2425
- const { threads, activeThreadId, selectThread, createThread, deleteThread } = useThreads();
2443
+ const {
2444
+ threads,
2445
+ activeThreadId,
2446
+ selectThread,
2447
+ createThread,
2448
+ deleteThread,
2449
+ isLoading
2450
+ } = useThreads();
2426
2451
  const handleThreadClick = (threadId) => {
2427
2452
  if (threadId !== activeThreadId) {
2428
2453
  selectThread(threadId);
@@ -2473,7 +2498,7 @@ var ThreadList = ({
2473
2498
  ]
2474
2499
  }
2475
2500
  ) }),
2476
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: threads.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-4 text-center text-muted-foreground", children: emptyState || /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2501
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: isLoading && threads.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-8", children: /* @__PURE__ */ jsx(IconLoader2, { className: "size-5 animate-spin text-muted-foreground" }) }) : threads.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-4 text-center text-muted-foreground", children: emptyState || /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2477
2502
  /* @__PURE__ */ jsx(IconMessage, { className: "size-8 mx-auto opacity-50" }),
2478
2503
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: "No threads yet" }),
2479
2504
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: handleNewThread, children: "Start a conversation" })