PluginProbe
Extendify / 3.1.4
Extendify v3.1.4
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 0.7.0 All 126 releases
extendify / src / Agent / state / chat.js

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

141 lines 4.9 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 const storage = {
129 getItem: async () => await apiFetch({ path }),
130 setItem: async (_name, state) =>
131 await apiFetch({ path, method: 'POST', data: { state } }),
132 };
133
134 export const useChatStore = create()(
135 persist(devtools(state, { name: 'Extendify Agent Chat' }), {
136 name: `extendify-agent-chat-${window.extSharedData.siteId}`,
137 storage: createJSONStorage(() => storage),
138 skipHydration: true,
139 }),
140 );
141