PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / state / chat.js

chat.js in Extendify 3.1.5, at src/Agent/state/chat.js

151 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { buildToolMessages } from '@agent/lib/tool-messages';
2 import { isChangeSiteDesignWorkflowAvailable, makeId } from '@agent/lib/util';
3 import { useStatusStore } from '@agent/state/status';
4 import apiFetch from '@wordpress/api-fetch';
5 import { __ } from '@wordpress/i18n';
6 import { create } from 'zustand';
7 import { createJSONStorage, devtools, persist } from 'zustand/middleware';
8
9 const { chatHistory } = window.extAgentData;
10
11 const welcomeMessage = [
12 {
13 id: 1,
14 type: 'message',
15 details: {
16 role: 'assistant',
17 // translators: this is the initial message in the agent chat, welcoming the user. Keep it short and friendly and follow the same markdown format and emoji.
18 content: isChangeSiteDesignWorkflowAvailable()
19 ? __(
20 '#### Your site is ready 🎉\nWant to explore other website designs?',
21 'extendify-local',
22 )
23 : __(
24 '#### Your site is ready 🎉\nWant to explore other site colors?',
25 'extendify-local',
26 ),
27 },
28 },
29 ];
30 const state = (set, get) => ({
31 messages: chatHistory?.length ? chatHistory.toReversed() : welcomeMessage,
32 // API messages, back to the last finished workflow.
33 getCurrentMessages: ({ includeTools = true } = {}) => {
34 const messages = [];
35 let foundUserMessage = false;
36 for (const { type, details } of get().messages.toReversed()) {
37 const finished = ['completed', 'canceled'].includes(details.status);
38 if (type === 'workflow' && finished) break;
39 if (type === 'workflow-component' && finished) break;
40 if (type === 'message' && details.error) break;
41 // A call with no result leaves the model answering a phantom.
42 if (type === 'tool' && !('result' in details)) continue;
43 if (type === 'tool' && includeTools) {
44 // buildToolMessages returns [call, result]; push reversed so the
45 // final toReversed() restores call-before-result order.
46 for (const m of buildToolMessages(details).toReversed())
47 messages.push(m);
48 }
49 // This prevents a loop of assistant messages from being at the end
50 if (type === 'message' && details.role === 'user') {
51 foundUserMessage = true;
52 }
53 if (type === 'message' && !foundUserMessage) continue;
54 if (type === 'message') messages.push(details);
55 }
56 return messages.toReversed();
57 },
58 // Most recent consecutive runs — another workflow in between resets the
59 // memory.
60 getMessagesFor: (workflowId) => {
61 if (!workflowId) return [];
62 const segments = [];
63 let segment = [];
64 for (const { type, details } of get().messages) {
65 const finished = ['completed', 'canceled'].includes(details.status);
66 if (['workflow', 'workflow-component'].includes(type) && finished) {
67 segments.push({ workflowId: details.workflowId, segment });
68 segment = [];
69 continue;
70 }
71 // An error ended a run without a marker; what precedes it is dead.
72 if (type === 'message' && details.error) {
73 segment = [];
74 continue;
75 }
76 if (type === 'tool' && 'result' in details) {
77 segment.push(...buildToolMessages(details, { summarize: true }));
78 continue;
79 }
80 if (type === 'message') segment.push(details);
81 }
82 const broken = segments.findLastIndex(
83 (finished) => finished.workflowId !== workflowId,
84 );
85 return segments.slice(broken + 1).flatMap(({ segment }) => segment);
86 },
87 getLastAssistantMessage: () =>
88 get()?.messages?.findLast(
89 (message) =>
90 message.type === 'message' && message.details?.role === 'assistant',
91 ),
92 hasMessages: () => get().messages.length > 0,
93 addMessage: (type, details) => {
94 const id = makeId();
95 set((state) => {
96 // max 250 messages
97 const max = Math.max(0, state.messages.length - 249);
98 const next = { id, type, details };
99 return {
100 // { id: 1, type: message, details: { role: 'user', content: 'Hello' } }
101 // { id: 2, type: message, details: { role: 'assistant', content: 'Hi there!' } }
102 // { id: 3, type: workflow, details: { name: 'Workflow 1' } }
103 messages: [...state.messages.toSpliced(0, max), next],
104 };
105 });
106 // A real message supersedes any in-flight progress status.
107 useStatusStore.getState().clearStatuses();
108 return id;
109 },
110 updateMessage: (id, details) =>
111 set((state) => ({
112 messages: state.messages.map((message) =>
113 message.id === id
114 ? { ...message, details: { ...message.details, ...details } }
115 : message,
116 ),
117 })),
118 // pop messages all the way back to the last agent message
119 popMessage: () => {
120 set((state) => ({
121 messages: state.messages?.slice(0, -1) || [],
122 }));
123 },
124 clearMessages: () => set({ messages: [] }),
125 });
126
127 const path = '/extendify/v1/agent/chat-events';
128 let lastSave = Promise.resolve();
129 const storage = {
130 getItem: async () => await apiFetch({ path }),
131 setItem: (_name, state) => {
132 lastSave = apiFetch({ path, method: 'POST', data: { state } });
133 return lastSave;
134 },
135 };
136
137 // Await before navigating — an aborted in-flight save loses the newest messages.
138 export const flushChatStorage = (timeout = 5000) =>
139 Promise.race([
140 lastSave.catch(() => null),
141 new Promise((resolve) => setTimeout(resolve, timeout)),
142 ]);
143
144 export const useChatStore = create()(
145 persist(devtools(state, { name: 'Extendify Agent Chat' }), {
146 name: `extendify-agent-chat-${window.extSharedData.siteId}`,
147 storage: createJSONStorage(() => storage),
148 skipHydration: true,
149 }),
150 );
151