@melony/react 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -31,12 +31,12 @@ export default function App() {
31
31
  import { useMelony } from "@melony/react";
32
32
 
33
33
  function Controls() {
34
- const { sendEvent, isLoading } = useMelony();
34
+ const { send, streaming } = useMelony();
35
35
  return (
36
36
  <button
37
- disabled={isLoading}
37
+ disabled={streaming}
38
38
  onClick={() =>
39
- sendEvent({ type: "text", data: { content: "Hello!" } })
39
+ send({ type: "text", data: { content: "Hello!" } })
40
40
  }
41
41
  >
42
42
  Send
@@ -49,26 +49,36 @@ function Controls() {
49
49
 
50
50
  - **`MelonyProvider`** + **`useMelony()`**
51
51
  - Connects to a Melony runtime endpoint.
52
- - Exposes `events`, `isLoading`, `error`, and `sendEvent()`.
52
+ - Exposes `messages`, `events`, `streaming`, `error`, and `send()`.
53
53
 
54
54
  ## Event Stream & UI
55
55
 
56
- Melony React provides the foundation for building interactive agent UIs. You can listen to the event stream via `useMelony()` and render your own components based on the event types.
56
+ Melony React provides the foundation for building interactive agent UIs. It automatically aggregates raw events into `messages` for easy rendering of chat interfaces.
57
57
 
58
58
  ```tsx
59
- const { events } = useMelony();
59
+ const { messages } = useMelony();
60
60
 
61
61
  return (
62
62
  <div>
63
- {events.map(event => {
64
- if (event.type === "text") return <Text key={event.id} content={event.data.content} />;
65
- if (event.type === "custom-ui") return <MyCustomUI key={event.id} {...event.data} />;
66
- return null;
67
- })}
63
+ {messages.map(message => (
64
+ <div key={message.runId} className={message.role}>
65
+ {message.content.map(event => {
66
+ if (event.type === "assistant:text-delta") {
67
+ return <span key={event.id}>{event.data.delta}</span>;
68
+ }
69
+ if (event.type === "ui") {
70
+ return <MyCustomUI key={event.id} {...event.data} />;
71
+ }
72
+ return null;
73
+ })}
74
+ </div>
75
+ ))}
68
76
  </div>
69
77
  );
70
78
  ```
71
79
 
80
+ You can still access raw `events` if you need lower-level control.
81
+
72
82
  ## Development
73
83
 
74
84
  ```bash
package/dist/index.cjs CHANGED
@@ -6,9 +6,11 @@ var jsxRuntime = require('react/jsx-runtime');
6
6
  // src/providers/melony-provider.tsx
7
7
 
8
8
  // src/utils/message-converter.ts
9
- var defaultGetRole = (e) => e.meta?.role || "assistant";
10
- var defaultGetRunId = (e) => e.meta?.runId;
11
- var defaultGetThreadId = (e) => e.meta?.threadId || e.meta?.state?.threadId;
9
+ var defaultGetRole = (e) => e.type === "user:text" ? "user" : "assistant";
10
+ var defaultGetRunId = (e, context) => {
11
+ return context?.runId;
12
+ };
13
+ var defaultGetThreadId = (e) => e.data?.threadId;
12
14
  var defaultShouldStartNewMessage = (event, current, utils) => {
13
15
  if (!current) return true;
14
16
  const role = utils.getRole(event);
@@ -18,13 +20,7 @@ var defaultShouldStartNewMessage = (event, current, utils) => {
18
20
  return false;
19
21
  };
20
22
  var defaultProcessEvent = (event, current) => {
21
- if (event.type === "text-delta" && event.data?.delta) {
22
- current.content += event.data.delta;
23
- } else if (event.type === "text") {
24
- current.content += event.data?.content || event.data?.text || "";
25
- } else {
26
- current.uiEvents.push(event);
27
- }
23
+ current.content.push(event);
28
24
  };
29
25
  function convertEventsToAggregatedMessages(events, options = {}) {
30
26
  const getRole = options.getRole || defaultGetRole;
@@ -39,10 +35,9 @@ function convertEventsToAggregatedMessages(events, options = {}) {
39
35
  if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {
40
36
  currentMessage = {
41
37
  role: getRole(event),
42
- content: "",
38
+ content: [],
43
39
  runId: getRunId(event),
44
- threadId: getThreadId(event),
45
- uiEvents: []
40
+ threadId: getThreadId(event)
46
41
  };
47
42
  messages.push(currentMessage);
48
43
  }
@@ -91,8 +86,8 @@ var MelonyProvider = ({
91
86
  () => ({
92
87
  ...state,
93
88
  messages,
94
- sendEvent: async (event) => {
95
- const generator = client.sendEvent(event);
89
+ send: async (event) => {
90
+ const generator = client.send(event);
96
91
  for await (const _ of generator) {
97
92
  }
98
93
  },
@@ -110,6 +105,49 @@ var useMelony = () => {
110
105
  }
111
106
  return context;
112
107
  };
108
+ function useMelonyInit(url, params = {}) {
109
+ const [data, setData] = react.useState(null);
110
+ const [loading, setLoading] = react.useState(true);
111
+ const [error, setError] = react.useState(null);
112
+ react.useEffect(() => {
113
+ let isMounted = true;
114
+ async function init() {
115
+ try {
116
+ setLoading(true);
117
+ setError(null);
118
+ const searchParams = new URLSearchParams();
119
+ Object.entries(params).forEach(([key, value]) => {
120
+ if (value !== void 0) {
121
+ searchParams.append(key, String(value));
122
+ }
123
+ });
124
+ const queryString = searchParams.toString();
125
+ const fullUrl = queryString ? `${url}${url.includes("?") ? "&" : "?"}${queryString}` : url;
126
+ const response = await fetch(fullUrl);
127
+ if (!response.ok) {
128
+ throw new Error(`Failed to initialize: ${response.statusText}`);
129
+ }
130
+ const json = await response.json();
131
+ if (isMounted) {
132
+ setData(json);
133
+ }
134
+ } catch (err) {
135
+ if (isMounted) {
136
+ setError(err instanceof Error ? err : new Error(String(err)));
137
+ }
138
+ } finally {
139
+ if (isMounted) {
140
+ setLoading(false);
141
+ }
142
+ }
143
+ }
144
+ init();
145
+ return () => {
146
+ isMounted = false;
147
+ };
148
+ }, [url, JSON.stringify(params)]);
149
+ return { data, loading, error };
150
+ }
113
151
 
114
152
  exports.MelonyContext = MelonyContext;
115
153
  exports.MelonyProvider = MelonyProvider;
@@ -120,5 +158,6 @@ exports.defaultGetThreadId = defaultGetThreadId;
120
158
  exports.defaultProcessEvent = defaultProcessEvent;
121
159
  exports.defaultShouldStartNewMessage = defaultShouldStartNewMessage;
122
160
  exports.useMelony = useMelony;
161
+ exports.useMelonyInit = useMelonyInit;
123
162
  //# sourceMappingURL=index.cjs.map
124
163
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/message-converter.ts","../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts"],"names":["createContext","useState","useEffect","useMemo","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Event, Role } from \"melony\";\n\n/**\n * A message aggregated from multiple events.\n */\nexport interface AggregatedMessage {\n /** The role of the sender (user, assistant, or system) */\n role: Role;\n /** The text content of the message, aggregated from text events */\n content: string;\n /** The unique ID for the run that produced this message */\n runId?: string;\n /** The ID of the thread this message belongs to */\n threadId?: string;\n /** UI events (e.g. SDUI components) associated with this message */\n uiEvents: Event[];\n}\n\n/**\n * Options for configuring how events are aggregated into messages.\n */\nexport interface AggregateOptions<TEvent extends Event = Event> {\n /**\n * Custom logic to extract the role from an event.\n * Defaults to event.meta.role or 'assistant'.\n */\n getRole?: (event: TEvent) => Role;\n\n /**\n * Custom logic to extract the runId from an event.\n * Defaults to event.meta.runId.\n */\n getRunId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to extract the threadId from an event.\n * Defaults to event.meta.threadId.\n */\n getThreadId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to determine if a new message should be started.\n * By default, starts a new message if:\n * 1. There is no current message.\n * 2. The role of the event is different from the current message.\n * 3. The runId of the event is different from the current message's runId.\n */\n shouldStartNewMessage?: (\n event: TEvent,\n currentMessage: AggregatedMessage | null,\n options: { getRole: (e: TEvent) => Role; getRunId: (e: TEvent) => string | undefined }\n ) => boolean;\n\n /**\n * Custom logic to process an event and update the current message.\n * By default:\n * - 'text-delta' events append their delta to the message content.\n * - 'text' events append their content or text to the message content.\n * - All other events are added to the message's uiEvents array.\n */\n processEvent?: (\n event: TEvent,\n currentMessage: AggregatedMessage,\n ) => void;\n}\n\n/**\n * Default implementation for extracting role from an event.\n */\nexport const defaultGetRole = <T extends Event>(e: T): Role => e.meta?.role || \"assistant\";\n\n/**\n * Default implementation for extracting runId from an event.\n */\nexport const defaultGetRunId = <T extends Event>(e: T): string | undefined => e.meta?.runId;\n\n/**\n * Default implementation for extracting threadId from an event.\n */\nexport const defaultGetThreadId = <T extends Event>(e: T): string | undefined => e.meta?.threadId || e.meta?.state?.threadId;\n\n/**\n * Default logic for determining if a new message should start.\n */\nexport const defaultShouldStartNewMessage = <T extends Event>(\n event: T,\n current: AggregatedMessage | null,\n utils: { getRole: (e: T) => Role; getRunId: (e: T) => string | undefined; getThreadId: (e: T) => string | undefined }\n): boolean => {\n if (!current) return true;\n const role = utils.getRole(event);\n const runId = utils.getRunId(event);\n \n // Start new message if role changes\n if (current.role !== role) return true;\n \n // Start new message if runId changes (and both have runIds)\n if (runId && current.runId && runId !== current.runId) return true;\n \n return false;\n};\n\n/**\n * Default logic for processing an event into a message.\n */\nexport const defaultProcessEvent = <T extends Event>(event: T, current: AggregatedMessage): void => {\n if (event.type === \"text-delta\" && (event.data as any)?.delta) {\n current.content += (event.data as any).delta;\n } else if (event.type === \"text\") {\n current.content += (event.data as any)?.content || (event.data as any)?.text || \"\";\n } else {\n current.uiEvents.push(event);\n }\n};\n\n/**\n * Helper to aggregate a list of events into a list of messages.\n * This is useful for rendering a chat-like interface from a raw event stream.\n * \n * @param events The list of events to aggregate.\n * @param options Configuration for aggregation logic.\n * @returns An array of aggregated messages.\n */\nexport function convertEventsToAggregatedMessages<TEvent extends Event = Event>(\n events: TEvent[],\n options: AggregateOptions<TEvent> = {}\n): AggregatedMessage[] {\n const getRole = options.getRole || defaultGetRole;\n const getRunId = options.getRunId || defaultGetRunId;\n const getThreadId = options.getThreadId || defaultGetThreadId;\n const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;\n const processEvent = options.processEvent || defaultProcessEvent;\n\n if (events.length === 0) return [];\n\n const messages: AggregatedMessage[] = [];\n let currentMessage: AggregatedMessage | null = null;\n\n for (const event of events) {\n if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {\n currentMessage = {\n role: getRole(event),\n content: \"\",\n runId: getRunId(event),\n threadId: getThreadId(event),\n uiEvents: [],\n };\n messages.push(currentMessage);\n }\n\n if (currentMessage) {\n processEvent(event, currentMessage);\n \n // If current message didn't have a runId but this event does, update it\n if (!currentMessage.runId) {\n const runId = getRunId(event);\n if (runId) {\n currentMessage.runId = runId;\n }\n }\n\n // If current message didn't have a threadId but this event does, update it\n if (!currentMessage.threadId) {\n const threadId = getThreadId(event);\n if (threadId) {\n currentMessage.threadId = threadId;\n }\n }\n }\n }\n\n return messages;\n}\n","import React, {\n createContext,\n useEffect,\n useState,\n useMemo,\n ReactNode,\n} from \"react\";\nimport { MelonyClient, ClientState } from \"melony/client\";\nimport {\n Config,\n Event,\n} from \"melony\";\nimport { \n AggregatedMessage, \n AggregateOptions, \n convertEventsToAggregatedMessages \n} from \"../utils/message-converter\";\n\nexport interface MelonyContextValue extends ClientState {\n sendEvent: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\n messages: AggregatedMessage[];\n}\n\nexport const MelonyContext = createContext<MelonyContextValue | undefined>(\n undefined,\n);\n\nexport interface MelonyProviderProps {\n children: ReactNode;\n client: MelonyClient;\n initialEvents?: Event[];\n aggregationOptions?: AggregateOptions;\n}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\n aggregationOptions,\n}) => {\n const [state, setState] = useState<ClientState>(client.getState());\n\n // Handle initial events on mount only\n useEffect(() => {\n if (initialEvents?.length && client.getState().events.length === 0) {\n client.reset(initialEvents);\n }\n }, []); // Empty deps - run once on mount\n\n // Subscribe to state changes\n useEffect(() => {\n const unsubscribe = client.subscribe(setState);\n return unsubscribe;\n }, [client]);\n\n const messages = useMemo(\n () => convertEventsToAggregatedMessages(state.events, aggregationOptions),\n [state.events, aggregationOptions]\n );\n\n const contextValue = useMemo(\n () => ({\n ...state,\n messages,\n sendEvent: async (event: Event) => {\n const generator = client.sendEvent(event);\n // Consume the generator to ensure event processing completes\n for await (const _ of generator) {\n // Events are handled by the client subscription\n }\n },\n reset: client.reset.bind(client),\n client,\n }),\n [state, messages, client],\n );\n\n return (\n <MelonyContext.Provider value={contextValue}>\n {children}\n </MelonyContext.Provider>\n );\n};","import { useContext } from \"react\";\nimport { MelonyContext, MelonyContextValue } from \"@/providers/melony-provider\";\n\nexport const useMelony = (): MelonyContextValue => {\n const context = useContext(MelonyContext);\n if (context === undefined) {\n throw new Error(\"useMelony must be used within a MelonyProvider\");\n }\n\n return context;\n};\n"]}
1
+ {"version":3,"sources":["../src/utils/message-converter.ts","../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts","../src/hooks/use-melony-init.ts"],"names":["createContext","useState","useEffect","useMemo","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Event, RuntimeContext } from \"melony\";\n\n/**\n * A message aggregated from multiple events.\n */\nexport interface AggregatedMessage {\n /** The role of the sender (user, assistant, or system) */\n role: string;\n /** The content of the message, containing the events */\n content: Event[];\n /** The unique ID for the run that produced this message */\n runId?: string;\n /** The ID of the thread this message belongs to */\n threadId?: string;\n}\n\n/**\n * Options for configuring how events are aggregated into messages.\n */\nexport interface AggregateOptions<TEvent extends Event = Event> {\n /**\n * Custom logic to extract the role from an event.\n * Defaults to event.meta.role or 'assistant'.\n */\n getRole?: (event: TEvent) => string;\n\n /**\n * Custom logic to extract the runId from an event.\n * Defaults to event.meta.runId.\n */\n getRunId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to extract the threadId from an event.\n * Defaults to event.meta.threadId.\n */\n getThreadId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to determine if a new message should be started.\n * By default, starts a new message if:\n * 1. There is no current message.\n * 2. The role of the event is different from the current message.\n * 3. The runId of the event is different from the current message's runId.\n */\n shouldStartNewMessage?: (\n event: TEvent,\n currentMessage: AggregatedMessage | null,\n options: { getRole: (e: TEvent) => string; getRunId: (e: TEvent) => string | undefined }\n ) => boolean;\n\n /**\n * Custom logic to process an event and update the current message.\n * By default, events are simply added to the message's content array.\n */\n processEvent?: (\n event: TEvent,\n currentMessage: AggregatedMessage,\n ) => void;\n}\n\n/**\n * Default implementation for extracting role from an event.\n */\nexport const defaultGetRole = <T extends Event>(e: T): string => e.type === \"user:text\" ? \"user\" : \"assistant\";\n\n/**\n * Default implementation for extracting runId from an event.\n */\nexport const defaultGetRunId = <T extends Event>(e: T, context?: RuntimeContext<any, T>): string | undefined => {\n return context?.runId;\n};\n\n/**\n * Default implementation for extracting threadId from an event.\n */\nexport const defaultGetThreadId = <T extends Event>(e: T): string | undefined => e.data?.threadId;\n\n/**\n * Default logic for determining if a new message should start.\n */\nexport const defaultShouldStartNewMessage = <T extends Event>(\n event: T,\n current: AggregatedMessage | null,\n utils: { getRole: (e: T) => string; getRunId: (e: T) => string | undefined; getThreadId: (e: T) => string | undefined }\n): boolean => {\n if (!current) return true;\n const role = utils.getRole(event);\n const runId = utils.getRunId(event);\n \n // Start new message if role changes\n if (current.role !== role) return true;\n \n // Start new message if runId changes (and both have runIds)\n if (runId && current.runId && runId !== current.runId) return true;\n \n return false;\n};\n\n/**\n * Default logic for processing an event into a message.\n */\nexport const defaultProcessEvent = <T extends Event>(event: T, current: AggregatedMessage): void => {\n current.content.push(event);\n};\n\n/**\n * Helper to aggregate a list of events into a list of messages.\n * This is useful for rendering a chat-like interface from a raw event stream.\n * \n * @param events The list of events to aggregate.\n * @param options Configuration for aggregation logic.\n * @returns An array of aggregated messages.\n */\nexport function convertEventsToAggregatedMessages<TEvent extends Event = Event>(\n events: TEvent[],\n options: AggregateOptions<TEvent> = {}\n): AggregatedMessage[] {\n const getRole = options.getRole || defaultGetRole;\n const getRunId = options.getRunId || defaultGetRunId;\n const getThreadId = options.getThreadId || defaultGetThreadId;\n const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;\n const processEvent = options.processEvent || defaultProcessEvent;\n\n if (events.length === 0) return [];\n\n const messages: AggregatedMessage[] = [];\n let currentMessage: AggregatedMessage | null = null;\n\n for (const event of events) {\n if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {\n currentMessage = {\n role: getRole(event),\n content: [],\n runId: getRunId(event),\n threadId: getThreadId(event),\n };\n messages.push(currentMessage);\n }\n\n if (currentMessage) {\n processEvent(event, currentMessage);\n \n // If current message didn't have a runId but this event does, update it\n if (!currentMessage.runId) {\n const runId = getRunId(event);\n if (runId) {\n currentMessage.runId = runId;\n }\n }\n\n // If current message didn't have a threadId but this event does, update it\n if (!currentMessage.threadId) {\n const threadId = getThreadId(event);\n if (threadId) {\n currentMessage.threadId = threadId;\n }\n }\n }\n }\n\n return messages;\n}\n","import React, {\n createContext,\n useEffect,\n useState,\n useMemo,\n ReactNode,\n} from \"react\";\nimport { MelonyClient, ClientState } from \"melony/client\";\nimport {\n Config,\n Event,\n} from \"melony\";\nimport { \n AggregatedMessage, \n AggregateOptions, \n convertEventsToAggregatedMessages \n} from \"../utils/message-converter\";\n\nexport interface MelonyContextValue extends ClientState {\n send: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\n messages: AggregatedMessage[];\n}\n\nexport const MelonyContext = createContext<MelonyContextValue | undefined>(\n undefined,\n);\n\nexport interface MelonyProviderProps {\n children: ReactNode;\n client: MelonyClient;\n initialEvents?: Event[];\n aggregationOptions?: AggregateOptions;\n}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\n aggregationOptions,\n}) => {\n const [state, setState] = useState<ClientState>(client.getState());\n\n // Handle initial events on mount only\n useEffect(() => {\n if (initialEvents?.length && client.getState().events.length === 0) {\n client.reset(initialEvents);\n }\n }, []); // Empty deps - run once on mount\n\n // Subscribe to state changes\n useEffect(() => {\n const unsubscribe = client.subscribe(setState);\n return unsubscribe;\n }, [client]);\n\n const messages = useMemo(\n () => convertEventsToAggregatedMessages(state.events, aggregationOptions),\n [state.events, aggregationOptions]\n );\n\n const contextValue = useMemo(\n () => ({\n ...state,\n messages,\n send: async (event: Event) => {\n const generator = client.send(event);\n // Consume the generator to ensure event processing completes\n for await (const _ of generator) {\n // Events are handled by the client subscription\n }\n },\n reset: client.reset.bind(client),\n client,\n }),\n [state, messages, client],\n );\n\n return (\n <MelonyContext.Provider value={contextValue}>\n {children}\n </MelonyContext.Provider>\n );\n};","import { useContext } from \"react\";\nimport { MelonyContext, MelonyContextValue } from \"../providers/melony-provider\";\n\nexport const useMelony = (): MelonyContextValue => {\n const context = useContext(MelonyContext);\n if (context === undefined) {\n throw new Error(\"useMelony must be used within a MelonyProvider\");\n }\n\n return context;\n};\n","import { useEffect, useState } from \"react\";\n\n/**\n * A hook to initialize a Melony application by fetching the initial UI state.\n * \n * @param url The API endpoint to fetch the initial state from.\n * @param params Optional query parameters to include in the request.\n * @returns An object containing the UI data, loading state, and any error encountered.\n */\nexport function useMelonyInit<T = any>(\n url: string, \n params: Record<string, string | number | boolean | undefined> = {}\n) {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n let isMounted = true;\n\n async function init() {\n try {\n setLoading(true);\n setError(null);\n\n const searchParams = new URLSearchParams();\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n });\n \n const queryString = searchParams.toString();\n const fullUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url;\n \n const response = await fetch(fullUrl);\n if (!response.ok) {\n throw new Error(`Failed to initialize: ${response.statusText}`);\n }\n const json = await response.json();\n \n if (isMounted) {\n setData(json);\n }\n } catch (err) {\n if (isMounted) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n if (isMounted) {\n setLoading(false);\n }\n }\n }\n\n init();\n\n return () => {\n isMounted = false;\n };\n }, [url, JSON.stringify(params)]);\n\n return { data, loading, error };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -1,21 +1,19 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { ClientState, MelonyClient } from 'melony/client';
3
- import { Role, Event, Config } from 'melony';
3
+ import { Event, RuntimeContext, Config } from 'melony';
4
4
 
5
5
  /**
6
6
  * A message aggregated from multiple events.
7
7
  */
8
8
  interface AggregatedMessage {
9
9
  /** The role of the sender (user, assistant, or system) */
10
- role: Role;
11
- /** The text content of the message, aggregated from text events */
12
- content: string;
10
+ role: string;
11
+ /** The content of the message, containing the events */
12
+ content: Event[];
13
13
  /** The unique ID for the run that produced this message */
14
14
  runId?: string;
15
15
  /** The ID of the thread this message belongs to */
16
16
  threadId?: string;
17
- /** UI events (e.g. SDUI components) associated with this message */
18
- uiEvents: Event[];
19
17
  }
20
18
  /**
21
19
  * Options for configuring how events are aggregated into messages.
@@ -25,7 +23,7 @@ interface AggregateOptions<TEvent extends Event = Event> {
25
23
  * Custom logic to extract the role from an event.
26
24
  * Defaults to event.meta.role or 'assistant'.
27
25
  */
28
- getRole?: (event: TEvent) => Role;
26
+ getRole?: (event: TEvent) => string;
29
27
  /**
30
28
  * Custom logic to extract the runId from an event.
31
29
  * Defaults to event.meta.runId.
@@ -44,26 +42,23 @@ interface AggregateOptions<TEvent extends Event = Event> {
44
42
  * 3. The runId of the event is different from the current message's runId.
45
43
  */
46
44
  shouldStartNewMessage?: (event: TEvent, currentMessage: AggregatedMessage | null, options: {
47
- getRole: (e: TEvent) => Role;
45
+ getRole: (e: TEvent) => string;
48
46
  getRunId: (e: TEvent) => string | undefined;
49
47
  }) => boolean;
50
48
  /**
51
49
  * Custom logic to process an event and update the current message.
52
- * By default:
53
- * - 'text-delta' events append their delta to the message content.
54
- * - 'text' events append their content or text to the message content.
55
- * - All other events are added to the message's uiEvents array.
50
+ * By default, events are simply added to the message's content array.
56
51
  */
57
52
  processEvent?: (event: TEvent, currentMessage: AggregatedMessage) => void;
58
53
  }
59
54
  /**
60
55
  * Default implementation for extracting role from an event.
61
56
  */
62
- declare const defaultGetRole: <T extends Event>(e: T) => Role;
57
+ declare const defaultGetRole: <T extends Event>(e: T) => string;
63
58
  /**
64
59
  * Default implementation for extracting runId from an event.
65
60
  */
66
- declare const defaultGetRunId: <T extends Event>(e: T) => string | undefined;
61
+ declare const defaultGetRunId: <T extends Event>(e: T, context?: RuntimeContext<any, T>) => string | undefined;
67
62
  /**
68
63
  * Default implementation for extracting threadId from an event.
69
64
  */
@@ -72,7 +67,7 @@ declare const defaultGetThreadId: <T extends Event>(e: T) => string | undefined;
72
67
  * Default logic for determining if a new message should start.
73
68
  */
74
69
  declare const defaultShouldStartNewMessage: <T extends Event>(event: T, current: AggregatedMessage | null, utils: {
75
- getRole: (e: T) => Role;
70
+ getRole: (e: T) => string;
76
71
  getRunId: (e: T) => string | undefined;
77
72
  getThreadId: (e: T) => string | undefined;
78
73
  }) => boolean;
@@ -91,7 +86,7 @@ declare const defaultProcessEvent: <T extends Event>(event: T, current: Aggregat
91
86
  declare function convertEventsToAggregatedMessages<TEvent extends Event = Event>(events: TEvent[], options?: AggregateOptions<TEvent>): AggregatedMessage[];
92
87
 
93
88
  interface MelonyContextValue extends ClientState {
94
- sendEvent: (event: Event) => Promise<void>;
89
+ send: (event: Event) => Promise<void>;
95
90
  reset: (events?: Event[]) => void;
96
91
  client: MelonyClient;
97
92
  config?: Config;
@@ -108,4 +103,17 @@ declare const MelonyProvider: React.FC<MelonyProviderProps>;
108
103
 
109
104
  declare const useMelony: () => MelonyContextValue;
110
105
 
111
- export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
106
+ /**
107
+ * A hook to initialize a Melony application by fetching the initial UI state.
108
+ *
109
+ * @param url The API endpoint to fetch the initial state from.
110
+ * @param params Optional query parameters to include in the request.
111
+ * @returns An object containing the UI data, loading state, and any error encountered.
112
+ */
113
+ declare function useMelonyInit<T = any>(url: string, params?: Record<string, string | number | boolean | undefined>): {
114
+ data: T | null;
115
+ loading: boolean;
116
+ error: Error | null;
117
+ };
118
+
119
+ export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony, useMelonyInit };
package/dist/index.d.ts CHANGED
@@ -1,21 +1,19 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { ClientState, MelonyClient } from 'melony/client';
3
- import { Role, Event, Config } from 'melony';
3
+ import { Event, RuntimeContext, Config } from 'melony';
4
4
 
5
5
  /**
6
6
  * A message aggregated from multiple events.
7
7
  */
8
8
  interface AggregatedMessage {
9
9
  /** The role of the sender (user, assistant, or system) */
10
- role: Role;
11
- /** The text content of the message, aggregated from text events */
12
- content: string;
10
+ role: string;
11
+ /** The content of the message, containing the events */
12
+ content: Event[];
13
13
  /** The unique ID for the run that produced this message */
14
14
  runId?: string;
15
15
  /** The ID of the thread this message belongs to */
16
16
  threadId?: string;
17
- /** UI events (e.g. SDUI components) associated with this message */
18
- uiEvents: Event[];
19
17
  }
20
18
  /**
21
19
  * Options for configuring how events are aggregated into messages.
@@ -25,7 +23,7 @@ interface AggregateOptions<TEvent extends Event = Event> {
25
23
  * Custom logic to extract the role from an event.
26
24
  * Defaults to event.meta.role or 'assistant'.
27
25
  */
28
- getRole?: (event: TEvent) => Role;
26
+ getRole?: (event: TEvent) => string;
29
27
  /**
30
28
  * Custom logic to extract the runId from an event.
31
29
  * Defaults to event.meta.runId.
@@ -44,26 +42,23 @@ interface AggregateOptions<TEvent extends Event = Event> {
44
42
  * 3. The runId of the event is different from the current message's runId.
45
43
  */
46
44
  shouldStartNewMessage?: (event: TEvent, currentMessage: AggregatedMessage | null, options: {
47
- getRole: (e: TEvent) => Role;
45
+ getRole: (e: TEvent) => string;
48
46
  getRunId: (e: TEvent) => string | undefined;
49
47
  }) => boolean;
50
48
  /**
51
49
  * Custom logic to process an event and update the current message.
52
- * By default:
53
- * - 'text-delta' events append their delta to the message content.
54
- * - 'text' events append their content or text to the message content.
55
- * - All other events are added to the message's uiEvents array.
50
+ * By default, events are simply added to the message's content array.
56
51
  */
57
52
  processEvent?: (event: TEvent, currentMessage: AggregatedMessage) => void;
58
53
  }
59
54
  /**
60
55
  * Default implementation for extracting role from an event.
61
56
  */
62
- declare const defaultGetRole: <T extends Event>(e: T) => Role;
57
+ declare const defaultGetRole: <T extends Event>(e: T) => string;
63
58
  /**
64
59
  * Default implementation for extracting runId from an event.
65
60
  */
66
- declare const defaultGetRunId: <T extends Event>(e: T) => string | undefined;
61
+ declare const defaultGetRunId: <T extends Event>(e: T, context?: RuntimeContext<any, T>) => string | undefined;
67
62
  /**
68
63
  * Default implementation for extracting threadId from an event.
69
64
  */
@@ -72,7 +67,7 @@ declare const defaultGetThreadId: <T extends Event>(e: T) => string | undefined;
72
67
  * Default logic for determining if a new message should start.
73
68
  */
74
69
  declare const defaultShouldStartNewMessage: <T extends Event>(event: T, current: AggregatedMessage | null, utils: {
75
- getRole: (e: T) => Role;
70
+ getRole: (e: T) => string;
76
71
  getRunId: (e: T) => string | undefined;
77
72
  getThreadId: (e: T) => string | undefined;
78
73
  }) => boolean;
@@ -91,7 +86,7 @@ declare const defaultProcessEvent: <T extends Event>(event: T, current: Aggregat
91
86
  declare function convertEventsToAggregatedMessages<TEvent extends Event = Event>(events: TEvent[], options?: AggregateOptions<TEvent>): AggregatedMessage[];
92
87
 
93
88
  interface MelonyContextValue extends ClientState {
94
- sendEvent: (event: Event) => Promise<void>;
89
+ send: (event: Event) => Promise<void>;
95
90
  reset: (events?: Event[]) => void;
96
91
  client: MelonyClient;
97
92
  config?: Config;
@@ -108,4 +103,17 @@ declare const MelonyProvider: React.FC<MelonyProviderProps>;
108
103
 
109
104
  declare const useMelony: () => MelonyContextValue;
110
105
 
111
- export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
106
+ /**
107
+ * A hook to initialize a Melony application by fetching the initial UI state.
108
+ *
109
+ * @param url The API endpoint to fetch the initial state from.
110
+ * @param params Optional query parameters to include in the request.
111
+ * @returns An object containing the UI data, loading state, and any error encountered.
112
+ */
113
+ declare function useMelonyInit<T = any>(url: string, params?: Record<string, string | number | boolean | undefined>): {
114
+ data: T | null;
115
+ loading: boolean;
116
+ error: Error | null;
117
+ };
118
+
119
+ export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony, useMelonyInit };
package/dist/index.js CHANGED
@@ -4,9 +4,11 @@ import { jsx } from 'react/jsx-runtime';
4
4
  // src/providers/melony-provider.tsx
5
5
 
6
6
  // src/utils/message-converter.ts
7
- var defaultGetRole = (e) => e.meta?.role || "assistant";
8
- var defaultGetRunId = (e) => e.meta?.runId;
9
- var defaultGetThreadId = (e) => e.meta?.threadId || e.meta?.state?.threadId;
7
+ var defaultGetRole = (e) => e.type === "user:text" ? "user" : "assistant";
8
+ var defaultGetRunId = (e, context) => {
9
+ return context?.runId;
10
+ };
11
+ var defaultGetThreadId = (e) => e.data?.threadId;
10
12
  var defaultShouldStartNewMessage = (event, current, utils) => {
11
13
  if (!current) return true;
12
14
  const role = utils.getRole(event);
@@ -16,13 +18,7 @@ var defaultShouldStartNewMessage = (event, current, utils) => {
16
18
  return false;
17
19
  };
18
20
  var defaultProcessEvent = (event, current) => {
19
- if (event.type === "text-delta" && event.data?.delta) {
20
- current.content += event.data.delta;
21
- } else if (event.type === "text") {
22
- current.content += event.data?.content || event.data?.text || "";
23
- } else {
24
- current.uiEvents.push(event);
25
- }
21
+ current.content.push(event);
26
22
  };
27
23
  function convertEventsToAggregatedMessages(events, options = {}) {
28
24
  const getRole = options.getRole || defaultGetRole;
@@ -37,10 +33,9 @@ function convertEventsToAggregatedMessages(events, options = {}) {
37
33
  if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {
38
34
  currentMessage = {
39
35
  role: getRole(event),
40
- content: "",
36
+ content: [],
41
37
  runId: getRunId(event),
42
- threadId: getThreadId(event),
43
- uiEvents: []
38
+ threadId: getThreadId(event)
44
39
  };
45
40
  messages.push(currentMessage);
46
41
  }
@@ -89,8 +84,8 @@ var MelonyProvider = ({
89
84
  () => ({
90
85
  ...state,
91
86
  messages,
92
- sendEvent: async (event) => {
93
- const generator = client.sendEvent(event);
87
+ send: async (event) => {
88
+ const generator = client.send(event);
94
89
  for await (const _ of generator) {
95
90
  }
96
91
  },
@@ -108,7 +103,50 @@ var useMelony = () => {
108
103
  }
109
104
  return context;
110
105
  };
106
+ function useMelonyInit(url, params = {}) {
107
+ const [data, setData] = useState(null);
108
+ const [loading, setLoading] = useState(true);
109
+ const [error, setError] = useState(null);
110
+ useEffect(() => {
111
+ let isMounted = true;
112
+ async function init() {
113
+ try {
114
+ setLoading(true);
115
+ setError(null);
116
+ const searchParams = new URLSearchParams();
117
+ Object.entries(params).forEach(([key, value]) => {
118
+ if (value !== void 0) {
119
+ searchParams.append(key, String(value));
120
+ }
121
+ });
122
+ const queryString = searchParams.toString();
123
+ const fullUrl = queryString ? `${url}${url.includes("?") ? "&" : "?"}${queryString}` : url;
124
+ const response = await fetch(fullUrl);
125
+ if (!response.ok) {
126
+ throw new Error(`Failed to initialize: ${response.statusText}`);
127
+ }
128
+ const json = await response.json();
129
+ if (isMounted) {
130
+ setData(json);
131
+ }
132
+ } catch (err) {
133
+ if (isMounted) {
134
+ setError(err instanceof Error ? err : new Error(String(err)));
135
+ }
136
+ } finally {
137
+ if (isMounted) {
138
+ setLoading(false);
139
+ }
140
+ }
141
+ }
142
+ init();
143
+ return () => {
144
+ isMounted = false;
145
+ };
146
+ }, [url, JSON.stringify(params)]);
147
+ return { data, loading, error };
148
+ }
111
149
 
112
- export { MelonyContext, MelonyProvider, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
150
+ export { MelonyContext, MelonyProvider, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony, useMelonyInit };
113
151
  //# sourceMappingURL=index.js.map
114
152
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/message-converter.ts","../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Event, Role } from \"melony\";\n\n/**\n * A message aggregated from multiple events.\n */\nexport interface AggregatedMessage {\n /** The role of the sender (user, assistant, or system) */\n role: Role;\n /** The text content of the message, aggregated from text events */\n content: string;\n /** The unique ID for the run that produced this message */\n runId?: string;\n /** The ID of the thread this message belongs to */\n threadId?: string;\n /** UI events (e.g. SDUI components) associated with this message */\n uiEvents: Event[];\n}\n\n/**\n * Options for configuring how events are aggregated into messages.\n */\nexport interface AggregateOptions<TEvent extends Event = Event> {\n /**\n * Custom logic to extract the role from an event.\n * Defaults to event.meta.role or 'assistant'.\n */\n getRole?: (event: TEvent) => Role;\n\n /**\n * Custom logic to extract the runId from an event.\n * Defaults to event.meta.runId.\n */\n getRunId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to extract the threadId from an event.\n * Defaults to event.meta.threadId.\n */\n getThreadId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to determine if a new message should be started.\n * By default, starts a new message if:\n * 1. There is no current message.\n * 2. The role of the event is different from the current message.\n * 3. The runId of the event is different from the current message's runId.\n */\n shouldStartNewMessage?: (\n event: TEvent,\n currentMessage: AggregatedMessage | null,\n options: { getRole: (e: TEvent) => Role; getRunId: (e: TEvent) => string | undefined }\n ) => boolean;\n\n /**\n * Custom logic to process an event and update the current message.\n * By default:\n * - 'text-delta' events append their delta to the message content.\n * - 'text' events append their content or text to the message content.\n * - All other events are added to the message's uiEvents array.\n */\n processEvent?: (\n event: TEvent,\n currentMessage: AggregatedMessage,\n ) => void;\n}\n\n/**\n * Default implementation for extracting role from an event.\n */\nexport const defaultGetRole = <T extends Event>(e: T): Role => e.meta?.role || \"assistant\";\n\n/**\n * Default implementation for extracting runId from an event.\n */\nexport const defaultGetRunId = <T extends Event>(e: T): string | undefined => e.meta?.runId;\n\n/**\n * Default implementation for extracting threadId from an event.\n */\nexport const defaultGetThreadId = <T extends Event>(e: T): string | undefined => e.meta?.threadId || e.meta?.state?.threadId;\n\n/**\n * Default logic for determining if a new message should start.\n */\nexport const defaultShouldStartNewMessage = <T extends Event>(\n event: T,\n current: AggregatedMessage | null,\n utils: { getRole: (e: T) => Role; getRunId: (e: T) => string | undefined; getThreadId: (e: T) => string | undefined }\n): boolean => {\n if (!current) return true;\n const role = utils.getRole(event);\n const runId = utils.getRunId(event);\n \n // Start new message if role changes\n if (current.role !== role) return true;\n \n // Start new message if runId changes (and both have runIds)\n if (runId && current.runId && runId !== current.runId) return true;\n \n return false;\n};\n\n/**\n * Default logic for processing an event into a message.\n */\nexport const defaultProcessEvent = <T extends Event>(event: T, current: AggregatedMessage): void => {\n if (event.type === \"text-delta\" && (event.data as any)?.delta) {\n current.content += (event.data as any).delta;\n } else if (event.type === \"text\") {\n current.content += (event.data as any)?.content || (event.data as any)?.text || \"\";\n } else {\n current.uiEvents.push(event);\n }\n};\n\n/**\n * Helper to aggregate a list of events into a list of messages.\n * This is useful for rendering a chat-like interface from a raw event stream.\n * \n * @param events The list of events to aggregate.\n * @param options Configuration for aggregation logic.\n * @returns An array of aggregated messages.\n */\nexport function convertEventsToAggregatedMessages<TEvent extends Event = Event>(\n events: TEvent[],\n options: AggregateOptions<TEvent> = {}\n): AggregatedMessage[] {\n const getRole = options.getRole || defaultGetRole;\n const getRunId = options.getRunId || defaultGetRunId;\n const getThreadId = options.getThreadId || defaultGetThreadId;\n const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;\n const processEvent = options.processEvent || defaultProcessEvent;\n\n if (events.length === 0) return [];\n\n const messages: AggregatedMessage[] = [];\n let currentMessage: AggregatedMessage | null = null;\n\n for (const event of events) {\n if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {\n currentMessage = {\n role: getRole(event),\n content: \"\",\n runId: getRunId(event),\n threadId: getThreadId(event),\n uiEvents: [],\n };\n messages.push(currentMessage);\n }\n\n if (currentMessage) {\n processEvent(event, currentMessage);\n \n // If current message didn't have a runId but this event does, update it\n if (!currentMessage.runId) {\n const runId = getRunId(event);\n if (runId) {\n currentMessage.runId = runId;\n }\n }\n\n // If current message didn't have a threadId but this event does, update it\n if (!currentMessage.threadId) {\n const threadId = getThreadId(event);\n if (threadId) {\n currentMessage.threadId = threadId;\n }\n }\n }\n }\n\n return messages;\n}\n","import React, {\n createContext,\n useEffect,\n useState,\n useMemo,\n ReactNode,\n} from \"react\";\nimport { MelonyClient, ClientState } from \"melony/client\";\nimport {\n Config,\n Event,\n} from \"melony\";\nimport { \n AggregatedMessage, \n AggregateOptions, \n convertEventsToAggregatedMessages \n} from \"../utils/message-converter\";\n\nexport interface MelonyContextValue extends ClientState {\n sendEvent: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\n messages: AggregatedMessage[];\n}\n\nexport const MelonyContext = createContext<MelonyContextValue | undefined>(\n undefined,\n);\n\nexport interface MelonyProviderProps {\n children: ReactNode;\n client: MelonyClient;\n initialEvents?: Event[];\n aggregationOptions?: AggregateOptions;\n}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\n aggregationOptions,\n}) => {\n const [state, setState] = useState<ClientState>(client.getState());\n\n // Handle initial events on mount only\n useEffect(() => {\n if (initialEvents?.length && client.getState().events.length === 0) {\n client.reset(initialEvents);\n }\n }, []); // Empty deps - run once on mount\n\n // Subscribe to state changes\n useEffect(() => {\n const unsubscribe = client.subscribe(setState);\n return unsubscribe;\n }, [client]);\n\n const messages = useMemo(\n () => convertEventsToAggregatedMessages(state.events, aggregationOptions),\n [state.events, aggregationOptions]\n );\n\n const contextValue = useMemo(\n () => ({\n ...state,\n messages,\n sendEvent: async (event: Event) => {\n const generator = client.sendEvent(event);\n // Consume the generator to ensure event processing completes\n for await (const _ of generator) {\n // Events are handled by the client subscription\n }\n },\n reset: client.reset.bind(client),\n client,\n }),\n [state, messages, client],\n );\n\n return (\n <MelonyContext.Provider value={contextValue}>\n {children}\n </MelonyContext.Provider>\n );\n};","import { useContext } from \"react\";\nimport { MelonyContext, MelonyContextValue } from \"@/providers/melony-provider\";\n\nexport const useMelony = (): MelonyContextValue => {\n const context = useContext(MelonyContext);\n if (context === undefined) {\n throw new Error(\"useMelony must be used within a MelonyProvider\");\n }\n\n return context;\n};\n"]}
1
+ {"version":3,"sources":["../src/utils/message-converter.ts","../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts","../src/hooks/use-melony-init.ts"],"names":["useState","useEffect"],"mappings":"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","file":"index.js","sourcesContent":["import { Event, RuntimeContext } from \"melony\";\n\n/**\n * A message aggregated from multiple events.\n */\nexport interface AggregatedMessage {\n /** The role of the sender (user, assistant, or system) */\n role: string;\n /** The content of the message, containing the events */\n content: Event[];\n /** The unique ID for the run that produced this message */\n runId?: string;\n /** The ID of the thread this message belongs to */\n threadId?: string;\n}\n\n/**\n * Options for configuring how events are aggregated into messages.\n */\nexport interface AggregateOptions<TEvent extends Event = Event> {\n /**\n * Custom logic to extract the role from an event.\n * Defaults to event.meta.role or 'assistant'.\n */\n getRole?: (event: TEvent) => string;\n\n /**\n * Custom logic to extract the runId from an event.\n * Defaults to event.meta.runId.\n */\n getRunId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to extract the threadId from an event.\n * Defaults to event.meta.threadId.\n */\n getThreadId?: (event: TEvent) => string | undefined;\n\n /**\n * Custom logic to determine if a new message should be started.\n * By default, starts a new message if:\n * 1. There is no current message.\n * 2. The role of the event is different from the current message.\n * 3. The runId of the event is different from the current message's runId.\n */\n shouldStartNewMessage?: (\n event: TEvent,\n currentMessage: AggregatedMessage | null,\n options: { getRole: (e: TEvent) => string; getRunId: (e: TEvent) => string | undefined }\n ) => boolean;\n\n /**\n * Custom logic to process an event and update the current message.\n * By default, events are simply added to the message's content array.\n */\n processEvent?: (\n event: TEvent,\n currentMessage: AggregatedMessage,\n ) => void;\n}\n\n/**\n * Default implementation for extracting role from an event.\n */\nexport const defaultGetRole = <T extends Event>(e: T): string => e.type === \"user:text\" ? \"user\" : \"assistant\";\n\n/**\n * Default implementation for extracting runId from an event.\n */\nexport const defaultGetRunId = <T extends Event>(e: T, context?: RuntimeContext<any, T>): string | undefined => {\n return context?.runId;\n};\n\n/**\n * Default implementation for extracting threadId from an event.\n */\nexport const defaultGetThreadId = <T extends Event>(e: T): string | undefined => e.data?.threadId;\n\n/**\n * Default logic for determining if a new message should start.\n */\nexport const defaultShouldStartNewMessage = <T extends Event>(\n event: T,\n current: AggregatedMessage | null,\n utils: { getRole: (e: T) => string; getRunId: (e: T) => string | undefined; getThreadId: (e: T) => string | undefined }\n): boolean => {\n if (!current) return true;\n const role = utils.getRole(event);\n const runId = utils.getRunId(event);\n \n // Start new message if role changes\n if (current.role !== role) return true;\n \n // Start new message if runId changes (and both have runIds)\n if (runId && current.runId && runId !== current.runId) return true;\n \n return false;\n};\n\n/**\n * Default logic for processing an event into a message.\n */\nexport const defaultProcessEvent = <T extends Event>(event: T, current: AggregatedMessage): void => {\n current.content.push(event);\n};\n\n/**\n * Helper to aggregate a list of events into a list of messages.\n * This is useful for rendering a chat-like interface from a raw event stream.\n * \n * @param events The list of events to aggregate.\n * @param options Configuration for aggregation logic.\n * @returns An array of aggregated messages.\n */\nexport function convertEventsToAggregatedMessages<TEvent extends Event = Event>(\n events: TEvent[],\n options: AggregateOptions<TEvent> = {}\n): AggregatedMessage[] {\n const getRole = options.getRole || defaultGetRole;\n const getRunId = options.getRunId || defaultGetRunId;\n const getThreadId = options.getThreadId || defaultGetThreadId;\n const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;\n const processEvent = options.processEvent || defaultProcessEvent;\n\n if (events.length === 0) return [];\n\n const messages: AggregatedMessage[] = [];\n let currentMessage: AggregatedMessage | null = null;\n\n for (const event of events) {\n if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {\n currentMessage = {\n role: getRole(event),\n content: [],\n runId: getRunId(event),\n threadId: getThreadId(event),\n };\n messages.push(currentMessage);\n }\n\n if (currentMessage) {\n processEvent(event, currentMessage);\n \n // If current message didn't have a runId but this event does, update it\n if (!currentMessage.runId) {\n const runId = getRunId(event);\n if (runId) {\n currentMessage.runId = runId;\n }\n }\n\n // If current message didn't have a threadId but this event does, update it\n if (!currentMessage.threadId) {\n const threadId = getThreadId(event);\n if (threadId) {\n currentMessage.threadId = threadId;\n }\n }\n }\n }\n\n return messages;\n}\n","import React, {\n createContext,\n useEffect,\n useState,\n useMemo,\n ReactNode,\n} from \"react\";\nimport { MelonyClient, ClientState } from \"melony/client\";\nimport {\n Config,\n Event,\n} from \"melony\";\nimport { \n AggregatedMessage, \n AggregateOptions, \n convertEventsToAggregatedMessages \n} from \"../utils/message-converter\";\n\nexport interface MelonyContextValue extends ClientState {\n send: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\n messages: AggregatedMessage[];\n}\n\nexport const MelonyContext = createContext<MelonyContextValue | undefined>(\n undefined,\n);\n\nexport interface MelonyProviderProps {\n children: ReactNode;\n client: MelonyClient;\n initialEvents?: Event[];\n aggregationOptions?: AggregateOptions;\n}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\n aggregationOptions,\n}) => {\n const [state, setState] = useState<ClientState>(client.getState());\n\n // Handle initial events on mount only\n useEffect(() => {\n if (initialEvents?.length && client.getState().events.length === 0) {\n client.reset(initialEvents);\n }\n }, []); // Empty deps - run once on mount\n\n // Subscribe to state changes\n useEffect(() => {\n const unsubscribe = client.subscribe(setState);\n return unsubscribe;\n }, [client]);\n\n const messages = useMemo(\n () => convertEventsToAggregatedMessages(state.events, aggregationOptions),\n [state.events, aggregationOptions]\n );\n\n const contextValue = useMemo(\n () => ({\n ...state,\n messages,\n send: async (event: Event) => {\n const generator = client.send(event);\n // Consume the generator to ensure event processing completes\n for await (const _ of generator) {\n // Events are handled by the client subscription\n }\n },\n reset: client.reset.bind(client),\n client,\n }),\n [state, messages, client],\n );\n\n return (\n <MelonyContext.Provider value={contextValue}>\n {children}\n </MelonyContext.Provider>\n );\n};","import { useContext } from \"react\";\nimport { MelonyContext, MelonyContextValue } from \"../providers/melony-provider\";\n\nexport const useMelony = (): MelonyContextValue => {\n const context = useContext(MelonyContext);\n if (context === undefined) {\n throw new Error(\"useMelony must be used within a MelonyProvider\");\n }\n\n return context;\n};\n","import { useEffect, useState } from \"react\";\n\n/**\n * A hook to initialize a Melony application by fetching the initial UI state.\n * \n * @param url The API endpoint to fetch the initial state from.\n * @param params Optional query parameters to include in the request.\n * @returns An object containing the UI data, loading state, and any error encountered.\n */\nexport function useMelonyInit<T = any>(\n url: string, \n params: Record<string, string | number | boolean | undefined> = {}\n) {\n const [data, setData] = useState<T | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n let isMounted = true;\n\n async function init() {\n try {\n setLoading(true);\n setError(null);\n\n const searchParams = new URLSearchParams();\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined) {\n searchParams.append(key, String(value));\n }\n });\n \n const queryString = searchParams.toString();\n const fullUrl = queryString ? `${url}${url.includes('?') ? '&' : '?'}${queryString}` : url;\n \n const response = await fetch(fullUrl);\n if (!response.ok) {\n throw new Error(`Failed to initialize: ${response.statusText}`);\n }\n const json = await response.json();\n \n if (isMounted) {\n setData(json);\n }\n } catch (err) {\n if (isMounted) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n } finally {\n if (isMounted) {\n setLoading(false);\n }\n }\n }\n\n init();\n\n return () => {\n isMounted = false;\n };\n }, [url, JSON.stringify(params)]);\n\n return { data, loading, error };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melony/react",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -33,7 +33,7 @@
33
33
  "react": ">=18"
34
34
  },
35
35
  "dependencies": {
36
- "melony": "^0.2.2"
36
+ "melony": "^0.2.4"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.1.12",