@applica-software-guru/persona-sdk 0.1.84 → 0.1.85

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.
Files changed (67) hide show
  1. package/README.md +104 -0
  2. package/dist/bundle.cjs.js +18 -18
  3. package/dist/bundle.cjs.js.map +1 -1
  4. package/dist/bundle.es.js +3345 -2891
  5. package/dist/bundle.es.js.map +1 -1
  6. package/dist/bundle.iife.js +17 -17
  7. package/dist/bundle.iife.js.map +1 -1
  8. package/dist/bundle.umd.js +18 -18
  9. package/dist/bundle.umd.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/messages.d.ts +2 -0
  13. package/dist/messages.d.ts.map +1 -1
  14. package/dist/protocol/webrtc.d.ts.map +1 -1
  15. package/dist/protocol/websocket.d.ts.map +1 -1
  16. package/dist/runtime/context.d.ts +34 -0
  17. package/dist/runtime/context.d.ts.map +1 -0
  18. package/dist/runtime/handlers.d.ts +21 -0
  19. package/dist/runtime/handlers.d.ts.map +1 -0
  20. package/dist/runtime/listeners.d.ts +6 -0
  21. package/dist/runtime/listeners.d.ts.map +1 -0
  22. package/dist/runtime/protocols.d.ts +17 -0
  23. package/dist/runtime/protocols.d.ts.map +1 -0
  24. package/dist/runtime/threads.d.ts +35 -0
  25. package/dist/runtime/threads.d.ts.map +1 -0
  26. package/dist/runtime/utils.d.ts +10 -0
  27. package/dist/runtime/utils.d.ts.map +1 -0
  28. package/dist/runtime.d.ts +4 -22
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/storage/base.d.ts +19 -0
  31. package/dist/storage/base.d.ts.map +1 -0
  32. package/dist/storage/index.d.ts +3 -0
  33. package/dist/storage/index.d.ts.map +1 -0
  34. package/dist/storage/persona.d.ts +30 -0
  35. package/dist/storage/persona.d.ts.map +1 -0
  36. package/dist/types.d.ts +51 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +4 -3
  39. package/playground/src/chat.tsx +51 -66
  40. package/playground/src/components/assistant-ui/thread-list.tsx +45 -12
  41. package/playground/src/components/assistant-ui/thread.tsx +34 -96
  42. package/playground/src/components/assistant-ui/threadlist-sidebar.tsx +59 -0
  43. package/playground/src/components/chat/logging.tsx +53 -0
  44. package/playground/src/components/ui/input.tsx +21 -0
  45. package/playground/src/components/ui/separator.tsx +26 -0
  46. package/playground/src/components/ui/sheet.tsx +139 -0
  47. package/playground/src/components/ui/sidebar.tsx +619 -0
  48. package/playground/src/components/ui/skeleton.tsx +13 -0
  49. package/playground/src/components/ui/tooltip.tsx +0 -2
  50. package/playground/src/hooks/theme.ts +70 -0
  51. package/playground/src/hooks/use-mobile.ts +19 -0
  52. package/src/index.ts +1 -0
  53. package/src/messages.ts +98 -8
  54. package/src/protocol/webrtc.ts +1 -0
  55. package/src/protocol/websocket.ts +5 -2
  56. package/src/runtime/context.ts +88 -0
  57. package/src/runtime/handlers.ts +276 -0
  58. package/src/runtime/index.ts +6 -0
  59. package/src/runtime/listeners.ts +79 -0
  60. package/src/runtime/protocols.ts +169 -0
  61. package/src/runtime/threads.ts +120 -0
  62. package/src/runtime/utils.ts +46 -0
  63. package/src/runtime.tsx +226 -326
  64. package/src/storage/base.ts +21 -0
  65. package/src/storage/index.ts +2 -0
  66. package/src/storage/persona.ts +132 -0
  67. package/src/types.ts +64 -2
@@ -0,0 +1,70 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ type Theme = 'light' | 'dark' | 'auto';
4
+ const STORAGE_KEY = 'persona_theme_preference';
5
+
6
+ function getSystemTheme(): 'light' | 'dark' {
7
+ if (typeof window === 'undefined' || !window.matchMedia) return 'light';
8
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
9
+ }
10
+
11
+ export function useColorScheme() {
12
+ const [theme, setTheme] = useState<Theme>(() => {
13
+ try {
14
+ const stored = localStorage.getItem(STORAGE_KEY);
15
+ return (stored as Theme) || 'auto';
16
+ } catch {
17
+ return 'auto';
18
+ }
19
+ });
20
+
21
+ const apply = useCallback((t: Theme) => {
22
+ const root = document.documentElement;
23
+ const effective = t === 'auto' ? getSystemTheme() : t;
24
+ if (effective === 'dark') root.classList.add('dark');
25
+ else root.classList.remove('dark');
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ // Apply on mount and whenever theme changes
30
+ apply(theme);
31
+
32
+ // Listen to system changes when in 'auto' mode
33
+ let mql: MediaQueryList | null = null;
34
+ const onChange = () => {
35
+ if (theme === 'auto') apply('auto');
36
+ };
37
+
38
+ if (typeof window !== 'undefined' && window.matchMedia) {
39
+ mql = window.matchMedia('(prefers-color-scheme: dark)');
40
+ if (mql.addEventListener) mql.addEventListener('change', onChange);
41
+ else mql.addListener(onChange as any);
42
+ }
43
+
44
+ return () => {
45
+ if (!mql) return;
46
+ if (mql.removeEventListener) mql.removeEventListener('change', onChange);
47
+ else mql.removeListener(onChange as any);
48
+ };
49
+ }, [theme, apply]);
50
+
51
+ const setThemeAndPersist = useCallback((t: Theme) => {
52
+ try {
53
+ localStorage.setItem(STORAGE_KEY, t);
54
+ } catch {}
55
+ setTheme(t);
56
+ }, []);
57
+
58
+ const toggle = useCallback(() => {
59
+ console.log('Toggling theme from');
60
+ setTheme((prev) => {
61
+ const next = prev === 'dark' ? 'light' : prev === 'light' ? 'auto' : 'dark';
62
+ try {
63
+ localStorage.setItem(STORAGE_KEY, next);
64
+ } catch {}
65
+ return next;
66
+ });
67
+ }, []);
68
+
69
+ return { theme, setTheme: setThemeAndPersist, toggle } as const;
70
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange)
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/index.ts CHANGED
@@ -3,3 +3,4 @@ export * from './logging';
3
3
  export * from './protocol';
4
4
  export * from './types';
5
5
  export * from './tools';
6
+ export * from './storage';
package/src/messages.ts CHANGED
@@ -1,5 +1,30 @@
1
1
  import { PersonaMessage } from './types';
2
- import { FileMessagePart, ThreadMessageLike } from '@assistant-ui/react';
2
+ import { FileMessagePart, ThreadMessageLike, MessageStatus } from '@assistant-ui/react';
3
+
4
+ let messageIdCounter = 0;
5
+
6
+ export function generateMessageId(): string {
7
+ return `msg_${Date.now()}_${messageIdCounter++}`;
8
+ }
9
+
10
+ export function ensureMessageId(message: PersonaMessage): PersonaMessage {
11
+ // Convert thought to reasoning type
12
+ if (message.thought) {
13
+ message = { ...message, type: 'reasoning' };
14
+ }
15
+
16
+ if (!message.id) {
17
+ return {
18
+ ...message,
19
+ id: generateMessageId(),
20
+ createdAt: message.createdAt || new Date(),
21
+ };
22
+ }
23
+ if (!message.createdAt) {
24
+ return { ...message, createdAt: new Date() };
25
+ }
26
+ return message;
27
+ }
3
28
 
4
29
  function removeEmptyMessages(messages: PersonaMessage[]): PersonaMessage[] {
5
30
  return messages.filter((message) => {
@@ -13,10 +38,14 @@ function parseMessages(messages: PersonaMessage[]): PersonaMessage[] {
13
38
  const outputMessages: PersonaMessage[] = [];
14
39
  let currentMessage: PersonaMessage | null = null;
15
40
 
16
- for (const message of messages) {
41
+ for (let message of messages) {
42
+ // Ensure every message has an ID
43
+ message = ensureMessageId(message);
44
+
17
45
  if (message.type === 'transaction') {
18
46
  continue;
19
47
  }
48
+
20
49
  if (message.file) {
21
50
  if (currentMessage) {
22
51
  outputMessages.push(currentMessage);
@@ -39,11 +68,20 @@ function parseMessages(messages: PersonaMessage[]): PersonaMessage[] {
39
68
  currentMessage &&
40
69
  message.type === currentMessage.type &&
41
70
  message.protocol === currentMessage.protocol &&
42
- currentMessage.role === message.role
71
+ currentMessage.role === message.role &&
72
+ // CRITICAL: Only merge if the new message has the same ID (streaming chunks)
73
+ // OR if the current message doesn't have a finishReason yet (still streaming)
74
+ // This preserves branches while allowing streaming to work
75
+ (message.id === currentMessage.id || !currentMessage.finishReason)
43
76
  ) {
77
+ // Merge streaming chunks
44
78
  currentMessage.text += message.text;
45
79
  currentMessage.file = currentMessage.file || message.file;
80
+ currentMessage.finishReason = message.finishReason || currentMessage.finishReason;
81
+ // Keep the ID of the first chunk
82
+ currentMessage.id = currentMessage.id || message.id;
46
83
  } else {
84
+ // Different message or current is complete - don't merge
47
85
  if (currentMessage) {
48
86
  outputMessages.push(currentMessage);
49
87
  }
@@ -73,29 +111,81 @@ function convertMessage(message: PersonaMessage): ThreadMessageLike {
73
111
  ]
74
112
  : [];
75
113
 
114
+ // Determine message status for assistant messages
115
+ let messageStatus: MessageStatus | undefined;
116
+
117
+ if (message.role === 'assistant' || message.role === 'function') {
118
+ if (message.status) {
119
+ // Use existing status if provided
120
+ messageStatus = message.status as any;
121
+ } else if (message.finishReason === 'stop') {
122
+ messageStatus = { type: 'complete', reason: 'stop' } as const;
123
+ } else if (message.finishReason === 'function_call') {
124
+ messageStatus = { type: 'complete', reason: 'stop' } as const;
125
+ } else if (!message.finishReason) {
126
+ // Message is still being generated - this will trigger optimistic updates
127
+ messageStatus = undefined; // Let the runtime handle in-progress state
128
+ }
129
+ }
130
+
131
+ // Handle function/tool calls
76
132
  if (message.role === 'function') {
77
- return {
133
+ const result: ThreadMessageLike = {
78
134
  id: message.id!,
79
135
  role: 'assistant',
80
- status: message?.functionResponse === null ? { type: 'running' } : { type: 'complete', reason: 'stop' },
136
+ createdAt: message.createdAt,
81
137
  content:
82
138
  message.functionCalls?.map((call) => ({
83
139
  type: 'tool-call',
84
140
  toolName: call.name,
85
- toolCallId: call.id,
141
+ toolCallId: call.id || `tool_${Date.now()}`,
86
142
  args: call.args,
87
143
  result: message.functionResponse?.result,
88
144
  })) ?? [],
145
+ metadata: {
146
+ ...message.metadata,
147
+ custom: {
148
+ protocol: message.protocol,
149
+ sessionId: message.sessionId,
150
+ sources: message.sources,
151
+ ...message.metadata?.custom,
152
+ },
153
+ },
89
154
  };
155
+
156
+ if (messageStatus) {
157
+ return { ...result, status: messageStatus };
158
+ }
159
+ return result;
90
160
  }
91
- return {
161
+
162
+ // Handle regular messages
163
+ const baseMessage: ThreadMessageLike = {
92
164
  id: message.id!,
93
165
  role: message.role,
166
+ createdAt: message.createdAt,
94
167
  content:
95
168
  message.type === 'reasoning'
96
- ? [{ type: 'reasoning', text: message.text }, ...files]
169
+ ? [{ type: 'reasoning' as any, text: message.text }, ...files]
97
170
  : [{ type: 'text', text: message.text }, ...files],
171
+ metadata: {
172
+ ...message.metadata,
173
+ custom: {
174
+ protocol: message.protocol,
175
+ sessionId: message.sessionId,
176
+ sources: message.sources,
177
+ type: message.type,
178
+ ...message.metadata?.custom,
179
+ },
180
+ },
98
181
  };
182
+
183
+ // Add status only for assistant messages
184
+ if (message.role === 'assistant' && messageStatus) {
185
+ return { ...baseMessage, status: messageStatus };
186
+ }
187
+
188
+ return baseMessage;
99
189
  }
100
190
 
101
191
  export { parseMessages, convertMessage, removeEmptyMessages };
@@ -124,6 +124,7 @@ class PersonaWebRTCClient {
124
124
  const metadata = {
125
125
  apiKey: this.config.apiKey,
126
126
  agentId: this.config.agentId,
127
+ userId: this.config.userId || 'anonymous',
127
128
  sessionCode: session as string,
128
129
  };
129
130
  this.config.logger?.debug('Opening connection to WebRTC server: ', metadata);
@@ -49,7 +49,10 @@ class PersonaWebSocketProtocol extends PersonaProtocolBase {
49
49
 
50
50
  const apiKey = encodeURIComponent(this.config.apiKey);
51
51
  const agentId = this.config.agentId;
52
- const webSocketUrl = `${this.config.webSocketUrl}?sessionCode=${sid}&agentId=${agentId}&apiKey=${apiKey}`;
52
+ const userId = this.config.userId || 'anonymous';
53
+ const webSocketUrl = `${this.config.webSocketUrl}?sessionCode=${sid}&agentId=${agentId}&apiKey=${apiKey}&userId=${encodeURIComponent(
54
+ userId,
55
+ )}`;
53
56
  this.setStatus('connecting');
54
57
  this.webSocket = new WebSocket(webSocketUrl);
55
58
  this.webSocket.addEventListener('open', () => {
@@ -87,7 +90,7 @@ class PersonaWebSocketProtocol extends PersonaProtocolBase {
87
90
  public disconnect(): Promise<void> {
88
91
  this.config.logger?.debug('Disconnecting WebSocket');
89
92
  if (this.webSocket && this.status === 'connected') {
90
- this.webSocket.close();
93
+ this.webSocket.close(1000, 'Normal closure');
91
94
  this.setStatus('disconnected');
92
95
  this.webSocket = null;
93
96
  }
@@ -0,0 +1,88 @@
1
+ import { createContext, useContext } from 'react';
2
+ import { PersonaProtocol, ProtocolStatus } from '../types';
3
+ import { PersonaMessage } from '../types';
4
+ import { PersonaWebRTCProtocol } from '../protocol';
5
+ import { getFirstAvailableProtocolEndpoint } from './utils';
6
+
7
+ export type PersonaRuntimeContextType = {
8
+ protocols: PersonaProtocol[];
9
+ protocolsStatus: Map<string, ProtocolStatus>;
10
+ getMessages: () => PersonaMessage[];
11
+ };
12
+
13
+ export const PersonaRuntimeContext = createContext<PersonaRuntimeContextType | undefined>(undefined);
14
+
15
+ /**
16
+ * Hook to access the PersonaRuntime context
17
+ */
18
+ export function usePersonaRuntime(): PersonaRuntimeContextType {
19
+ const context = useContext(PersonaRuntimeContext);
20
+ if (!context) {
21
+ throw new Error('usePersonaRuntime must be used within a PersonaRuntimeProvider');
22
+ }
23
+ return context;
24
+ }
25
+
26
+ /**
27
+ * Retrieves a specific protocol instance from the PersonaRuntimeContext.
28
+ *
29
+ * @param protocol - The name of the protocol to use.
30
+ * @returns {PersonaProtocol | null} - The protocol instance or null if not found.
31
+ * @throws {Error} - If the hook is used outside of a PersonaRuntimeProvider.
32
+ */
33
+ export function usePersonaRuntimeProtocol(protocol: string): PersonaProtocol | null {
34
+ const context = useContext(PersonaRuntimeContext);
35
+ if (!context) {
36
+ throw new Error('usePersonaRuntimeProtocol must be used within a PersonaRuntimeProvider');
37
+ }
38
+
39
+ const protocolInstance = context.protocols.find((p) => p.getName() === protocol);
40
+ if (!protocolInstance) {
41
+ return null;
42
+ }
43
+
44
+ const status = context.protocolsStatus.get(protocolInstance.getName());
45
+
46
+ return {
47
+ ...protocolInstance,
48
+ connect: protocolInstance.connect.bind(protocolInstance),
49
+ disconnect: protocolInstance.disconnect.bind(protocolInstance),
50
+ sendPacket: protocolInstance.sendPacket.bind(protocolInstance),
51
+ setSession: protocolInstance.setSession.bind(protocolInstance),
52
+ addStatusChangeListener: protocolInstance.addStatusChangeListener.bind(protocolInstance),
53
+ addPacketListener: protocolInstance.addPacketListener.bind(protocolInstance),
54
+ getName: protocolInstance.getName.bind(protocolInstance),
55
+ getPriority: protocolInstance.getPriority.bind(protocolInstance),
56
+ status: status || protocolInstance.status,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Gets the endpoint URL of the first available protocol
62
+ */
63
+ export function usePersonaRuntimeEndpoint(): string | null {
64
+ const context = useContext(PersonaRuntimeContext);
65
+ if (!context) {
66
+ throw new Error('usePersonaRuntimeEndpoint must be used within a PersonaRuntimeProvider');
67
+ }
68
+
69
+ return getFirstAvailableProtocolEndpoint(context.protocols);
70
+ }
71
+
72
+ /**
73
+ * Gets the WebRTC protocol instance
74
+ */
75
+ export function usePersonaRuntimeWebRTCProtocol(): PersonaWebRTCProtocol | null {
76
+ return usePersonaRuntimeProtocol('webrtc') as PersonaWebRTCProtocol;
77
+ }
78
+
79
+ /**
80
+ * Gets all messages from the current conversation
81
+ */
82
+ export function usePersonaRuntimeMessages(): PersonaMessage[] {
83
+ const context = useContext(PersonaRuntimeContext);
84
+ if (!context) {
85
+ throw new Error('usePersonaRuntimeMessages must be used within a PersonaRuntimeProvider');
86
+ }
87
+ return context.getMessages();
88
+ }
@@ -0,0 +1,276 @@
1
+ import { AppendMessage } from '@assistant-ui/react';
2
+ import { StartRunConfig } from '@assistant-ui/react/dist/legacy-runtime/runtime-cores/core/ThreadRuntimeCore';
3
+ import { PersonaProtocol, PersonaMessage, TransformMessages } from '../types';
4
+ import type { PersonaLogger } from '../logging';
5
+ import { parseMessages } from '../messages';
6
+ import { fileToBase64 } from './utils';
7
+
8
+ /**
9
+ * Creates the onNew handler for new messages
10
+ */
11
+ export function createOnNewHandler(
12
+ protocols: PersonaProtocol[],
13
+ setMessages: React.Dispatch<React.SetStateAction<PersonaMessage[]>>,
14
+ setIsRunning: React.Dispatch<React.SetStateAction<boolean>>,
15
+ transformMessages?: TransformMessages,
16
+ logger?: PersonaLogger,
17
+ ) {
18
+ return async (message: AppendMessage) => {
19
+ if (message.content[0]?.type !== 'text') {
20
+ throw new Error('Only text messages are supported');
21
+ }
22
+
23
+ const ready = protocols.some((protocol) => protocol.getName() !== 'transaction' && protocol.status === 'connected');
24
+
25
+ if (!ready) {
26
+ setMessages((currentConversation) => {
27
+ const errorMessage: PersonaMessage = {
28
+ role: 'assistant',
29
+ type: 'text',
30
+ text: 'No protocol is connected.',
31
+ createdAt: new Date(),
32
+ finishReason: 'stop',
33
+ };
34
+ const newMessages = [...currentConversation, errorMessage];
35
+ return transformMessages ? transformMessages(newMessages) : newMessages;
36
+ });
37
+ return;
38
+ }
39
+
40
+ const input = message.content[0].text;
41
+ const userMessage: PersonaMessage = {
42
+ role: 'user',
43
+ type: 'text',
44
+ text: input,
45
+ createdAt: new Date(),
46
+ };
47
+
48
+ setMessages((currentConversation) => {
49
+ const newMessages = parseMessages([...currentConversation, userMessage]);
50
+ return transformMessages ? transformMessages(newMessages) : newMessages;
51
+ });
52
+
53
+ setIsRunning(true);
54
+
55
+ const protocol = protocols.sort((a, b) => b.getPriority() - a.getPriority()).find((protocol) => protocol.status === 'connected');
56
+
57
+ const content: Array<PersonaMessage> = [];
58
+
59
+ if (message.attachments) {
60
+ for (const attachment of message.attachments) {
61
+ if (attachment.contentType.startsWith('image/') && attachment.file) {
62
+ content.push({
63
+ role: 'user',
64
+ image: {
65
+ contentType: attachment.contentType,
66
+ content: await fileToBase64(attachment.file),
67
+ },
68
+ text: '',
69
+ type: 'text',
70
+ createdAt: new Date(),
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ if (message.content) {
77
+ content.push({
78
+ role: 'user',
79
+ text: message.content[0].text,
80
+ type: 'text',
81
+ createdAt: new Date(),
82
+ });
83
+ }
84
+
85
+ logger?.debug('Sending message:', content);
86
+ await protocol?.sendPacket({ type: 'request', payload: content });
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Creates the onEdit handler for editing messages
92
+ */
93
+ export function createOnEditHandler(
94
+ protocols: PersonaProtocol[],
95
+ messages: PersonaMessage[],
96
+ setMessages: React.Dispatch<React.SetStateAction<PersonaMessage[]>>,
97
+ setIsRunning: React.Dispatch<React.SetStateAction<boolean>>,
98
+ transformMessages?: TransformMessages,
99
+ logger?: PersonaLogger,
100
+ ) {
101
+ return async (message: AppendMessage) => {
102
+ if (message.content[0]?.type !== 'text') {
103
+ throw new Error('Only text messages are supported');
104
+ }
105
+
106
+ const protocol = protocols.sort((a, b) => b.getPriority() - a.getPriority()).find((protocol) => protocol.status === 'connected');
107
+
108
+ if (!protocol) {
109
+ logger?.debug('No protocol available for edit');
110
+ return;
111
+ }
112
+
113
+ const parentId = message.parentId;
114
+ if (!parentId) {
115
+ logger?.debug('No parent ID provided for edit');
116
+ return;
117
+ }
118
+
119
+ const parentIndex = messages.findIndex((m) => m.id === parentId);
120
+ if (parentIndex === -1) {
121
+ logger?.debug('Parent message not found:', parentId);
122
+ return;
123
+ }
124
+
125
+ const messagesToKeep = messages.slice(0, parentIndex + 1);
126
+ const editedMessage: PersonaMessage = {
127
+ role: 'user',
128
+ text: message.content[0].text,
129
+ type: 'text',
130
+ createdAt: new Date(),
131
+ };
132
+
133
+ const newMessages = parseMessages([...messagesToKeep, editedMessage]);
134
+ setMessages(transformMessages ? transformMessages(newMessages) : newMessages);
135
+
136
+ setIsRunning(true);
137
+
138
+ const content: Array<PersonaMessage> = [
139
+ {
140
+ role: 'user',
141
+ text: message.content[0].text,
142
+ type: 'text',
143
+ createdAt: new Date(),
144
+ },
145
+ ];
146
+
147
+ logger?.debug('Sending edited message:', content);
148
+ await protocol.sendPacket({ type: 'request', payload: content });
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Creates the onReload handler for regenerating messages
154
+ */
155
+ export function createOnReloadHandler(
156
+ protocols: PersonaProtocol[],
157
+ messages: PersonaMessage[],
158
+ setMessages: React.Dispatch<React.SetStateAction<PersonaMessage[]>>,
159
+ setIsRunning: React.Dispatch<React.SetStateAction<boolean>>,
160
+ transformMessages?: TransformMessages,
161
+ logger?: PersonaLogger,
162
+ ) {
163
+ return async (parentId: string | null, config: StartRunConfig) => {
164
+ const protocol = protocols.sort((a, b) => b.getPriority() - a.getPriority()).find((protocol) => protocol.status === 'connected');
165
+
166
+ if (!protocol) {
167
+ logger?.debug('No protocol available for reload');
168
+ return;
169
+ }
170
+
171
+ const actualParentId = config.parentId ?? parentId;
172
+ if (!actualParentId) {
173
+ logger?.debug('No parent ID provided for reload');
174
+ return;
175
+ }
176
+
177
+ const parentIndex = messages.findIndex((msg) => msg.id === actualParentId);
178
+ if (parentIndex === -1) {
179
+ logger?.debug('Parent message not found:', actualParentId);
180
+ return;
181
+ }
182
+
183
+ const parentMessage = messages[parentIndex];
184
+
185
+ setMessages((currentMessages) => {
186
+ const messagesToKeep = currentMessages.slice(0, parentIndex + 1);
187
+ return transformMessages ? transformMessages(messagesToKeep) : messagesToKeep;
188
+ });
189
+
190
+ let userMessage: PersonaMessage | undefined;
191
+ if (parentMessage.role === 'assistant' || parentMessage.role === 'function') {
192
+ for (let i = parentIndex - 1; i >= 0; i--) {
193
+ if (messages[i].role === 'user') {
194
+ userMessage = messages[i];
195
+ break;
196
+ }
197
+ }
198
+ } else if (parentMessage.role === 'user') {
199
+ userMessage = parentMessage;
200
+ }
201
+
202
+ if (!userMessage) {
203
+ logger?.debug('No user message found to regenerate from');
204
+ return;
205
+ }
206
+
207
+ setIsRunning(true);
208
+
209
+ const content: Array<PersonaMessage> = [];
210
+
211
+ if (userMessage.image) {
212
+ content.push({
213
+ role: 'user',
214
+ image: userMessage.image,
215
+ text: '',
216
+ type: 'text',
217
+ createdAt: new Date(),
218
+ });
219
+ }
220
+
221
+ content.push({
222
+ role: 'user',
223
+ text: userMessage.text,
224
+ type: 'text',
225
+ createdAt: new Date(),
226
+ });
227
+
228
+ logger?.debug('Regenerating message with config:', {
229
+ actualParentId,
230
+ sourceId: config.sourceId,
231
+ custom: config.runConfig.custom,
232
+ });
233
+ await protocol.sendPacket({ type: 'request', payload: content });
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Creates the onCancel handler for cancelling message generation
239
+ */
240
+ export function createOnCancelHandler(
241
+ protocols: PersonaProtocol[],
242
+ setMessages: React.Dispatch<React.SetStateAction<PersonaMessage[]>>,
243
+ setIsRunning: React.Dispatch<React.SetStateAction<boolean>>,
244
+ transformMessages?: TransformMessages,
245
+ ) {
246
+ return () => {
247
+ setIsRunning(false);
248
+
249
+ setMessages((currentMessages) => {
250
+ const lastMessage = currentMessages[currentMessages.length - 1];
251
+ if (lastMessage?.role === 'assistant' && !lastMessage.finishReason) {
252
+ const updatedMessages = [...currentMessages];
253
+ updatedMessages[updatedMessages.length - 1] = {
254
+ ...lastMessage,
255
+ finishReason: 'stop',
256
+ status: { type: 'incomplete' },
257
+ };
258
+ return transformMessages ? transformMessages(updatedMessages) : updatedMessages;
259
+ }
260
+ return currentMessages;
261
+ });
262
+
263
+ protocols.forEach((protocol) => {
264
+ if (protocol.status === 'connected') {
265
+ protocol
266
+ .sendPacket({
267
+ type: 'command',
268
+ payload: { command: 'set_initial_context', arguments: { cancel: true } },
269
+ })
270
+ .catch(() => {});
271
+ }
272
+ });
273
+
274
+ return Promise.resolve();
275
+ };
276
+ }
@@ -0,0 +1,6 @@
1
+ export * from './context';
2
+ export * from './utils';
3
+ export * from './protocols';
4
+ export * from './handlers';
5
+ export * from './listeners';
6
+ export * from './threads';