@melony/react 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -4,13 +4,74 @@ var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
 
6
6
  // src/providers/melony-provider.tsx
7
+
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;
12
+ var defaultShouldStartNewMessage = (event, current, utils) => {
13
+ if (!current) return true;
14
+ const role = utils.getRole(event);
15
+ const runId = utils.getRunId(event);
16
+ if (current.role !== role) return true;
17
+ if (runId && current.runId && runId !== current.runId) return true;
18
+ return false;
19
+ };
20
+ 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
+ }
28
+ };
29
+ function convertEventsToAggregatedMessages(events, options = {}) {
30
+ const getRole = options.getRole || defaultGetRole;
31
+ const getRunId = options.getRunId || defaultGetRunId;
32
+ const getThreadId = options.getThreadId || defaultGetThreadId;
33
+ const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;
34
+ const processEvent = options.processEvent || defaultProcessEvent;
35
+ if (events.length === 0) return [];
36
+ const messages = [];
37
+ let currentMessage = null;
38
+ for (const event of events) {
39
+ if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {
40
+ currentMessage = {
41
+ role: getRole(event),
42
+ content: "",
43
+ runId: getRunId(event),
44
+ threadId: getThreadId(event),
45
+ uiEvents: []
46
+ };
47
+ messages.push(currentMessage);
48
+ }
49
+ if (currentMessage) {
50
+ processEvent(event, currentMessage);
51
+ if (!currentMessage.runId) {
52
+ const runId = getRunId(event);
53
+ if (runId) {
54
+ currentMessage.runId = runId;
55
+ }
56
+ }
57
+ if (!currentMessage.threadId) {
58
+ const threadId = getThreadId(event);
59
+ if (threadId) {
60
+ currentMessage.threadId = threadId;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return messages;
66
+ }
7
67
  var MelonyContext = react.createContext(
8
68
  void 0
9
69
  );
10
70
  var MelonyProvider = ({
11
71
  children,
12
72
  client,
13
- initialEvents
73
+ initialEvents,
74
+ aggregationOptions
14
75
  }) => {
15
76
  const [state, setState] = react.useState(client.getState());
16
77
  react.useEffect(() => {
@@ -22,9 +83,14 @@ var MelonyProvider = ({
22
83
  const unsubscribe = client.subscribe(setState);
23
84
  return unsubscribe;
24
85
  }, [client]);
86
+ const messages = react.useMemo(
87
+ () => convertEventsToAggregatedMessages(state.events, aggregationOptions),
88
+ [state.events, aggregationOptions]
89
+ );
25
90
  const contextValue = react.useMemo(
26
91
  () => ({
27
92
  ...state,
93
+ messages,
28
94
  sendEvent: async (event) => {
29
95
  const generator = client.sendEvent(event);
30
96
  for await (const _ of generator) {
@@ -33,7 +99,7 @@ var MelonyProvider = ({
33
99
  reset: client.reset.bind(client),
34
100
  client
35
101
  }),
36
- [state, client]
102
+ [state, messages, client]
37
103
  );
38
104
  return /* @__PURE__ */ jsxRuntime.jsx(MelonyContext.Provider, { value: contextValue, children });
39
105
  };
@@ -47,6 +113,12 @@ var useMelony = () => {
47
113
 
48
114
  exports.MelonyContext = MelonyContext;
49
115
  exports.MelonyProvider = MelonyProvider;
116
+ exports.convertEventsToAggregatedMessages = convertEventsToAggregatedMessages;
117
+ exports.defaultGetRole = defaultGetRole;
118
+ exports.defaultGetRunId = defaultGetRunId;
119
+ exports.defaultGetThreadId = defaultGetThreadId;
120
+ exports.defaultProcessEvent = defaultProcessEvent;
121
+ exports.defaultShouldStartNewMessage = defaultShouldStartNewMessage;
50
122
  exports.useMelony = useMelony;
51
123
  //# sourceMappingURL=index.cjs.map
52
124
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts"],"names":["createContext","useState","useEffect","useMemo","useContext"],"mappings":";;;;;;AAoBO,IAAM,aAAA,GAAgBA,mBAAA;AAAA,EAC3B;AACF;AAQO,IAAM,iBAAgD,CAAC;AAAA,EAC5D,QAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,KAAM;AACJ,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,IAAIC,cAAA,CAAsB,MAAA,CAAO,UAAU,CAAA;AAGjE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,eAAe,MAAA,IAAU,MAAA,CAAO,UAAS,CAAE,MAAA,CAAO,WAAW,CAAA,EAAG;AAClE,MAAA,MAAA,CAAO,MAAM,aAAa,CAAA;AAAA,IAC5B;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAGL,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,SAAA,CAAU,QAAQ,CAAA;AAC7C,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,YAAA,GAAeC,aAAA;AAAA,IACnB,OAAO;AAAA,MACL,GAAG,KAAA;AAAA,MACH,SAAA,EAAW,OAAO,KAAA,KAAiB;AACjC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA;AAExC,QAAA,WAAA,MAAiB,KAAK,SAAA,EAAW;AAAA,QAEjC;AAAA,MACF,CAAA;AAAA,MACA,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,MAC/B;AAAA,KACF,CAAA;AAAA,IACA,CAAC,OAAO,MAAM;AAAA,GAChB;AAEA,EAAA,sCACG,aAAA,CAAc,QAAA,EAAd,EAAuB,KAAA,EAAO,cAC5B,QAAA,EACH,CAAA;AAEJ;ACpEO,IAAM,YAAY,MAA0B;AACjD,EAAA,MAAM,OAAA,GAAUC,iBAAW,aAAa,CAAA;AACxC,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,EAClE;AAEA,EAAA,OAAO,OAAA;AACT","file":"index.cjs","sourcesContent":["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\";\n\nexport interface MelonyContextValue extends ClientState {\n sendEvent: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\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}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\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 contextValue = useMemo(\n () => ({\n ...state,\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, 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"],"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"]}
package/dist/index.d.cts CHANGED
@@ -1,21 +1,111 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { ClientState, MelonyClient } from 'melony/client';
3
- import { Event, Config } from 'melony';
3
+ import { Role, Event, Config } from 'melony';
4
+
5
+ /**
6
+ * A message aggregated from multiple events.
7
+ */
8
+ interface AggregatedMessage {
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;
13
+ /** The unique ID for the run that produced this message */
14
+ runId?: string;
15
+ /** The ID of the thread this message belongs to */
16
+ threadId?: string;
17
+ /** UI events (e.g. SDUI components) associated with this message */
18
+ uiEvents: Event[];
19
+ }
20
+ /**
21
+ * Options for configuring how events are aggregated into messages.
22
+ */
23
+ interface AggregateOptions<TEvent extends Event = Event> {
24
+ /**
25
+ * Custom logic to extract the role from an event.
26
+ * Defaults to event.meta.role or 'assistant'.
27
+ */
28
+ getRole?: (event: TEvent) => Role;
29
+ /**
30
+ * Custom logic to extract the runId from an event.
31
+ * Defaults to event.meta.runId.
32
+ */
33
+ getRunId?: (event: TEvent) => string | undefined;
34
+ /**
35
+ * Custom logic to extract the threadId from an event.
36
+ * Defaults to event.meta.threadId.
37
+ */
38
+ getThreadId?: (event: TEvent) => string | undefined;
39
+ /**
40
+ * Custom logic to determine if a new message should be started.
41
+ * By default, starts a new message if:
42
+ * 1. There is no current message.
43
+ * 2. The role of the event is different from the current message.
44
+ * 3. The runId of the event is different from the current message's runId.
45
+ */
46
+ shouldStartNewMessage?: (event: TEvent, currentMessage: AggregatedMessage | null, options: {
47
+ getRole: (e: TEvent) => Role;
48
+ getRunId: (e: TEvent) => string | undefined;
49
+ }) => boolean;
50
+ /**
51
+ * 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.
56
+ */
57
+ processEvent?: (event: TEvent, currentMessage: AggregatedMessage) => void;
58
+ }
59
+ /**
60
+ * Default implementation for extracting role from an event.
61
+ */
62
+ declare const defaultGetRole: <T extends Event>(e: T) => Role;
63
+ /**
64
+ * Default implementation for extracting runId from an event.
65
+ */
66
+ declare const defaultGetRunId: <T extends Event>(e: T) => string | undefined;
67
+ /**
68
+ * Default implementation for extracting threadId from an event.
69
+ */
70
+ declare const defaultGetThreadId: <T extends Event>(e: T) => string | undefined;
71
+ /**
72
+ * Default logic for determining if a new message should start.
73
+ */
74
+ declare const defaultShouldStartNewMessage: <T extends Event>(event: T, current: AggregatedMessage | null, utils: {
75
+ getRole: (e: T) => Role;
76
+ getRunId: (e: T) => string | undefined;
77
+ getThreadId: (e: T) => string | undefined;
78
+ }) => boolean;
79
+ /**
80
+ * Default logic for processing an event into a message.
81
+ */
82
+ declare const defaultProcessEvent: <T extends Event>(event: T, current: AggregatedMessage) => void;
83
+ /**
84
+ * Helper to aggregate a list of events into a list of messages.
85
+ * This is useful for rendering a chat-like interface from a raw event stream.
86
+ *
87
+ * @param events The list of events to aggregate.
88
+ * @param options Configuration for aggregation logic.
89
+ * @returns An array of aggregated messages.
90
+ */
91
+ declare function convertEventsToAggregatedMessages<TEvent extends Event = Event>(events: TEvent[], options?: AggregateOptions<TEvent>): AggregatedMessage[];
4
92
 
5
93
  interface MelonyContextValue extends ClientState {
6
94
  sendEvent: (event: Event) => Promise<void>;
7
95
  reset: (events?: Event[]) => void;
8
96
  client: MelonyClient;
9
97
  config?: Config;
98
+ messages: AggregatedMessage[];
10
99
  }
11
100
  declare const MelonyContext: React.Context<MelonyContextValue | undefined>;
12
101
  interface MelonyProviderProps {
13
102
  children: ReactNode;
14
103
  client: MelonyClient;
15
104
  initialEvents?: Event[];
105
+ aggregationOptions?: AggregateOptions;
16
106
  }
17
107
  declare const MelonyProvider: React.FC<MelonyProviderProps>;
18
108
 
19
109
  declare const useMelony: () => MelonyContextValue;
20
110
 
21
- export { MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, useMelony };
111
+ export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
package/dist/index.d.ts CHANGED
@@ -1,21 +1,111 @@
1
1
  import React, { ReactNode } from 'react';
2
2
  import { ClientState, MelonyClient } from 'melony/client';
3
- import { Event, Config } from 'melony';
3
+ import { Role, Event, Config } from 'melony';
4
+
5
+ /**
6
+ * A message aggregated from multiple events.
7
+ */
8
+ interface AggregatedMessage {
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;
13
+ /** The unique ID for the run that produced this message */
14
+ runId?: string;
15
+ /** The ID of the thread this message belongs to */
16
+ threadId?: string;
17
+ /** UI events (e.g. SDUI components) associated with this message */
18
+ uiEvents: Event[];
19
+ }
20
+ /**
21
+ * Options for configuring how events are aggregated into messages.
22
+ */
23
+ interface AggregateOptions<TEvent extends Event = Event> {
24
+ /**
25
+ * Custom logic to extract the role from an event.
26
+ * Defaults to event.meta.role or 'assistant'.
27
+ */
28
+ getRole?: (event: TEvent) => Role;
29
+ /**
30
+ * Custom logic to extract the runId from an event.
31
+ * Defaults to event.meta.runId.
32
+ */
33
+ getRunId?: (event: TEvent) => string | undefined;
34
+ /**
35
+ * Custom logic to extract the threadId from an event.
36
+ * Defaults to event.meta.threadId.
37
+ */
38
+ getThreadId?: (event: TEvent) => string | undefined;
39
+ /**
40
+ * Custom logic to determine if a new message should be started.
41
+ * By default, starts a new message if:
42
+ * 1. There is no current message.
43
+ * 2. The role of the event is different from the current message.
44
+ * 3. The runId of the event is different from the current message's runId.
45
+ */
46
+ shouldStartNewMessage?: (event: TEvent, currentMessage: AggregatedMessage | null, options: {
47
+ getRole: (e: TEvent) => Role;
48
+ getRunId: (e: TEvent) => string | undefined;
49
+ }) => boolean;
50
+ /**
51
+ * 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.
56
+ */
57
+ processEvent?: (event: TEvent, currentMessage: AggregatedMessage) => void;
58
+ }
59
+ /**
60
+ * Default implementation for extracting role from an event.
61
+ */
62
+ declare const defaultGetRole: <T extends Event>(e: T) => Role;
63
+ /**
64
+ * Default implementation for extracting runId from an event.
65
+ */
66
+ declare const defaultGetRunId: <T extends Event>(e: T) => string | undefined;
67
+ /**
68
+ * Default implementation for extracting threadId from an event.
69
+ */
70
+ declare const defaultGetThreadId: <T extends Event>(e: T) => string | undefined;
71
+ /**
72
+ * Default logic for determining if a new message should start.
73
+ */
74
+ declare const defaultShouldStartNewMessage: <T extends Event>(event: T, current: AggregatedMessage | null, utils: {
75
+ getRole: (e: T) => Role;
76
+ getRunId: (e: T) => string | undefined;
77
+ getThreadId: (e: T) => string | undefined;
78
+ }) => boolean;
79
+ /**
80
+ * Default logic for processing an event into a message.
81
+ */
82
+ declare const defaultProcessEvent: <T extends Event>(event: T, current: AggregatedMessage) => void;
83
+ /**
84
+ * Helper to aggregate a list of events into a list of messages.
85
+ * This is useful for rendering a chat-like interface from a raw event stream.
86
+ *
87
+ * @param events The list of events to aggregate.
88
+ * @param options Configuration for aggregation logic.
89
+ * @returns An array of aggregated messages.
90
+ */
91
+ declare function convertEventsToAggregatedMessages<TEvent extends Event = Event>(events: TEvent[], options?: AggregateOptions<TEvent>): AggregatedMessage[];
4
92
 
5
93
  interface MelonyContextValue extends ClientState {
6
94
  sendEvent: (event: Event) => Promise<void>;
7
95
  reset: (events?: Event[]) => void;
8
96
  client: MelonyClient;
9
97
  config?: Config;
98
+ messages: AggregatedMessage[];
10
99
  }
11
100
  declare const MelonyContext: React.Context<MelonyContextValue | undefined>;
12
101
  interface MelonyProviderProps {
13
102
  children: ReactNode;
14
103
  client: MelonyClient;
15
104
  initialEvents?: Event[];
105
+ aggregationOptions?: AggregateOptions;
16
106
  }
17
107
  declare const MelonyProvider: React.FC<MelonyProviderProps>;
18
108
 
19
109
  declare const useMelony: () => MelonyContextValue;
20
110
 
21
- export { MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, useMelony };
111
+ export { type AggregateOptions, type AggregatedMessage, MelonyContext, type MelonyContextValue, MelonyProvider, type MelonyProviderProps, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
package/dist/index.js CHANGED
@@ -2,13 +2,74 @@ import { createContext, useState, useEffect, useMemo, useContext } from 'react';
2
2
  import { jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/providers/melony-provider.tsx
5
+
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;
10
+ var defaultShouldStartNewMessage = (event, current, utils) => {
11
+ if (!current) return true;
12
+ const role = utils.getRole(event);
13
+ const runId = utils.getRunId(event);
14
+ if (current.role !== role) return true;
15
+ if (runId && current.runId && runId !== current.runId) return true;
16
+ return false;
17
+ };
18
+ 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
+ }
26
+ };
27
+ function convertEventsToAggregatedMessages(events, options = {}) {
28
+ const getRole = options.getRole || defaultGetRole;
29
+ const getRunId = options.getRunId || defaultGetRunId;
30
+ const getThreadId = options.getThreadId || defaultGetThreadId;
31
+ const shouldStartNewMessage = options.shouldStartNewMessage || defaultShouldStartNewMessage;
32
+ const processEvent = options.processEvent || defaultProcessEvent;
33
+ if (events.length === 0) return [];
34
+ const messages = [];
35
+ let currentMessage = null;
36
+ for (const event of events) {
37
+ if (shouldStartNewMessage(event, currentMessage, { getRole, getRunId, getThreadId })) {
38
+ currentMessage = {
39
+ role: getRole(event),
40
+ content: "",
41
+ runId: getRunId(event),
42
+ threadId: getThreadId(event),
43
+ uiEvents: []
44
+ };
45
+ messages.push(currentMessage);
46
+ }
47
+ if (currentMessage) {
48
+ processEvent(event, currentMessage);
49
+ if (!currentMessage.runId) {
50
+ const runId = getRunId(event);
51
+ if (runId) {
52
+ currentMessage.runId = runId;
53
+ }
54
+ }
55
+ if (!currentMessage.threadId) {
56
+ const threadId = getThreadId(event);
57
+ if (threadId) {
58
+ currentMessage.threadId = threadId;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return messages;
64
+ }
5
65
  var MelonyContext = createContext(
6
66
  void 0
7
67
  );
8
68
  var MelonyProvider = ({
9
69
  children,
10
70
  client,
11
- initialEvents
71
+ initialEvents,
72
+ aggregationOptions
12
73
  }) => {
13
74
  const [state, setState] = useState(client.getState());
14
75
  useEffect(() => {
@@ -20,9 +81,14 @@ var MelonyProvider = ({
20
81
  const unsubscribe = client.subscribe(setState);
21
82
  return unsubscribe;
22
83
  }, [client]);
84
+ const messages = useMemo(
85
+ () => convertEventsToAggregatedMessages(state.events, aggregationOptions),
86
+ [state.events, aggregationOptions]
87
+ );
23
88
  const contextValue = useMemo(
24
89
  () => ({
25
90
  ...state,
91
+ messages,
26
92
  sendEvent: async (event) => {
27
93
  const generator = client.sendEvent(event);
28
94
  for await (const _ of generator) {
@@ -31,7 +97,7 @@ var MelonyProvider = ({
31
97
  reset: client.reset.bind(client),
32
98
  client
33
99
  }),
34
- [state, client]
100
+ [state, messages, client]
35
101
  );
36
102
  return /* @__PURE__ */ jsx(MelonyContext.Provider, { value: contextValue, children });
37
103
  };
@@ -43,6 +109,6 @@ var useMelony = () => {
43
109
  return context;
44
110
  };
45
111
 
46
- export { MelonyContext, MelonyProvider, useMelony };
112
+ export { MelonyContext, MelonyProvider, convertEventsToAggregatedMessages, defaultGetRole, defaultGetRunId, defaultGetThreadId, defaultProcessEvent, defaultShouldStartNewMessage, useMelony };
47
113
  //# sourceMappingURL=index.js.map
48
114
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/providers/melony-provider.tsx","../src/hooks/use-melony.ts"],"names":[],"mappings":";;;;AAoBO,IAAM,aAAA,GAAgB,aAAA;AAAA,EAC3B;AACF;AAQO,IAAM,iBAAgD,CAAC;AAAA,EAC5D,QAAA;AAAA,EACA,MAAA;AAAA,EACA;AACF,CAAA,KAAM;AACJ,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,IAAI,QAAA,CAAsB,MAAA,CAAO,UAAU,CAAA;AAGjE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,eAAe,MAAA,IAAU,MAAA,CAAO,UAAS,CAAE,MAAA,CAAO,WAAW,CAAA,EAAG;AAClE,MAAA,MAAA,CAAO,MAAM,aAAa,CAAA;AAAA,IAC5B;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,SAAA,CAAU,QAAQ,CAAA;AAC7C,IAAA,OAAO,WAAA;AAAA,EACT,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AAEX,EAAA,MAAM,YAAA,GAAe,OAAA;AAAA,IACnB,OAAO;AAAA,MACL,GAAG,KAAA;AAAA,MACH,SAAA,EAAW,OAAO,KAAA,KAAiB;AACjC,QAAA,MAAM,SAAA,GAAY,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA;AAExC,QAAA,WAAA,MAAiB,KAAK,SAAA,EAAW;AAAA,QAEjC;AAAA,MACF,CAAA;AAAA,MACA,KAAA,EAAO,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA;AAAA,MAC/B;AAAA,KACF,CAAA;AAAA,IACA,CAAC,OAAO,MAAM;AAAA,GAChB;AAEA,EAAA,2BACG,aAAA,CAAc,QAAA,EAAd,EAAuB,KAAA,EAAO,cAC5B,QAAA,EACH,CAAA;AAEJ;ACpEO,IAAM,YAAY,MAA0B;AACjD,EAAA,MAAM,OAAA,GAAU,WAAW,aAAa,CAAA;AACxC,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,EAClE;AAEA,EAAA,OAAO,OAAA;AACT","file":"index.js","sourcesContent":["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\";\n\nexport interface MelonyContextValue extends ClientState {\n sendEvent: (event: Event) => Promise<void>;\n reset: (events?: Event[]) => void;\n client: MelonyClient;\n config?: Config;\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}\n\nexport const MelonyProvider: React.FC<MelonyProviderProps> = ({\n children,\n client,\n initialEvents,\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 contextValue = useMemo(\n () => ({\n ...state,\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, 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"],"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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melony/react",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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.1"
36
+ "melony": "^0.2.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.1.12",