@melony/react 0.1.21 → 0.1.23

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 {
@@ -41,6 +42,7 @@ interface ComposerOptionGroup {
41
42
  label: string;
42
43
  options: ComposerOption[];
43
44
  type?: "single" | "multiple";
45
+ defaultSelectedIds?: string[];
44
46
  }
45
47
  interface AuthService {
46
48
  getMe: () => Promise<User | null>;
@@ -63,12 +65,18 @@ interface MelonyContextValue extends ClientState {
63
65
  }) => Promise<void>;
64
66
  reset: (events?: Event[]) => void;
65
67
  client: MelonyClient;
68
+ config?: {
69
+ starterPrompts: any[];
70
+ options: any[];
71
+ };
66
72
  }
67
73
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
68
74
  interface MelonyClientProviderProps {
69
75
  children: ReactNode;
70
76
  client: MelonyClient;
71
77
  initialEvents?: Event[];
78
+ queryClient?: QueryClient;
79
+ configApi?: string;
72
80
  }
73
81
  declare const MelonyClientProvider: React__default.FC<MelonyClientProviderProps>;
74
82
 
@@ -136,7 +144,7 @@ interface ThreadProps {
136
144
  autoFocus?: boolean;
137
145
  defaultSelectedIds?: string[];
138
146
  }
139
- declare function Thread({ className, placeholder, starterPrompts, onStarterPromptClick, options, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
147
+ declare function Thread({ className, placeholder, starterPrompts: localStarterPrompts, onStarterPromptClick, options: localOptions, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
140
148
 
141
149
  interface ComposerProps {
142
150
  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 {
@@ -41,6 +42,7 @@ interface ComposerOptionGroup {
41
42
  label: string;
42
43
  options: ComposerOption[];
43
44
  type?: "single" | "multiple";
45
+ defaultSelectedIds?: string[];
44
46
  }
45
47
  interface AuthService {
46
48
  getMe: () => Promise<User | null>;
@@ -63,12 +65,18 @@ interface MelonyContextValue extends ClientState {
63
65
  }) => Promise<void>;
64
66
  reset: (events?: Event[]) => void;
65
67
  client: MelonyClient;
68
+ config?: {
69
+ starterPrompts: any[];
70
+ options: any[];
71
+ };
66
72
  }
67
73
  declare const MelonyContext: React__default.Context<MelonyContextValue | undefined>;
68
74
  interface MelonyClientProviderProps {
69
75
  children: ReactNode;
70
76
  client: MelonyClient;
71
77
  initialEvents?: Event[];
78
+ queryClient?: QueryClient;
79
+ configApi?: string;
72
80
  }
73
81
  declare const MelonyClientProvider: React__default.FC<MelonyClientProviderProps>;
74
82
 
@@ -136,7 +144,7 @@ interface ThreadProps {
136
144
  autoFocus?: boolean;
137
145
  defaultSelectedIds?: string[];
138
146
  }
139
- declare function Thread({ className, placeholder, starterPrompts, onStarterPromptClick, options, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
147
+ declare function Thread({ className, placeholder, starterPrompts: localStarterPrompts, onStarterPromptClick, options: localOptions, autoFocus, defaultSelectedIds, }: ThreadProps): react_jsx_runtime.JSX.Element;
140
148
 
141
149
  interface ComposerProps {
142
150
  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,26 @@ 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;
2344
+ const allDefaultSelectedIds = useMemo(() => {
2345
+ const defaultSelectedIdsFromOptions = options?.flatMap(
2346
+ (group) => group.defaultSelectedIds ?? []
2347
+ ) ?? [];
2348
+ return [
2349
+ .../* @__PURE__ */ new Set([...defaultSelectedIdsFromOptions, ...defaultSelectedIds ?? []])
2350
+ ];
2351
+ }, [options, defaultSelectedIds]);
2326
2352
  const [input, setInput] = useState("");
2327
2353
  const messagesEndRef = useRef(null);
2328
2354
  useEffect(() => {
@@ -2338,7 +2364,7 @@ function Thread({
2338
2364
  role: "user",
2339
2365
  data: { content: text }
2340
2366
  },
2341
- { state }
2367
+ { state: { ...state, threadId: activeThreadId ?? void 0 } }
2342
2368
  );
2343
2369
  };
2344
2370
  const handleStarterPromptClick = (prompt) => {
@@ -2348,21 +2374,21 @@ function Thread({
2348
2374
  handleSubmit(void 0, prompt);
2349
2375
  }
2350
2376
  };
2351
- const showStarterPrompts = messages.length === 0 && starterPrompts && starterPrompts.length > 0;
2377
+ const showStarterPrompts = messages.length === 0 && starterPrompts && starterPrompts.length > 0 && !isLoadingEvents;
2352
2378
  return /* @__PURE__ */ jsxs(
2353
2379
  "div",
2354
2380
  {
2355
2381
  className: cn("relative flex flex-col h-full bg-background", className),
2356
2382
  children: [
2357
2383
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 pb-36", children: [
2358
- /* @__PURE__ */ jsxs(
2384
+ /* @__PURE__ */ jsx(
2359
2385
  "div",
2360
2386
  {
2361
2387
  className: cn(
2362
2388
  "max-w-4xl mx-auto w-full p-4",
2363
2389
  showStarterPrompts && "min-h-full flex flex-col"
2364
2390
  ),
2365
- children: [
2391
+ 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
2392
  showStarterPrompts && /* @__PURE__ */ jsx(
2367
2393
  StarterPrompts,
2368
2394
  {
@@ -2379,7 +2405,7 @@ function Thread({
2379
2405
  loadingStatus
2380
2406
  }
2381
2407
  )
2382
- ]
2408
+ ] })
2383
2409
  }
2384
2410
  ),
2385
2411
  /* @__PURE__ */ jsx("div", { ref: messagesEndRef })
@@ -2394,7 +2420,7 @@ function Thread({
2394
2420
  isLoading,
2395
2421
  options,
2396
2422
  autoFocus,
2397
- defaultSelectedIds
2423
+ defaultSelectedIds: allDefaultSelectedIds
2398
2424
  }
2399
2425
  ) }) })
2400
2426
  ]
@@ -2422,7 +2448,14 @@ var ThreadList = ({
2422
2448
  emptyState,
2423
2449
  onThreadSelect
2424
2450
  }) => {
2425
- const { threads, activeThreadId, selectThread, createThread, deleteThread } = useThreads();
2451
+ const {
2452
+ threads,
2453
+ activeThreadId,
2454
+ selectThread,
2455
+ createThread,
2456
+ deleteThread,
2457
+ isLoading
2458
+ } = useThreads();
2426
2459
  const handleThreadClick = (threadId) => {
2427
2460
  if (threadId !== activeThreadId) {
2428
2461
  selectThread(threadId);
@@ -2473,7 +2506,7 @@ var ThreadList = ({
2473
2506
  ]
2474
2507
  }
2475
2508
  ) }),
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: [
2509
+ /* @__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
2510
  /* @__PURE__ */ jsx(IconMessage, { className: "size-8 mx-auto opacity-50" }),
2478
2511
  /* @__PURE__ */ jsx("p", { className: "text-sm", children: "No threads yet" }),
2479
2512
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: handleNewThread, children: "Start a conversation" })