| 1 |
import apiFetch from '@wordpress/api-fetch'; |
| 2 |
import { create } from 'zustand'; |
| 3 |
import { persist, devtools, createJSONStorage } from 'zustand/middleware'; |
| 4 |
import { makeId } from '@agent/lib/util'; |
| 5 |
|
| 6 |
const { chatHistory } = window.extAgentData; |
| 7 |
const state = (set, get) => ({ |
| 8 |
messagesRaw: (chatHistory || []).toReversed(), |
| 9 |
messages: chatHistory?.length |
| 10 |
? chatHistory |
| 11 |
// Remove some noise on reload |
| 12 |
.filter( |
| 13 |
(message) => |
| 14 |
!['agent-working', 'calling-agent', 'tool-started'].includes( |
| 15 |
message.details?.type, |
| 16 |
) && |
| 17 |
!( |
| 18 |
// Keep workflow messages with status completed (to show rating) |
| 19 |
( |
| 20 |
['workflow'].includes(message.type) && |
| 21 |
message.details.status === 'completed' |
| 22 |
) |
| 23 |
), |
| 24 |
) |
| 25 |
.toReversed() |
| 26 |
: [], |
| 27 |
|
| 28 |
seenAgents: [], |
| 29 |
// Messages sent to the api, user and assistant only. Up until the last workflow |
| 30 |
getMessagesForAI: () => { |
| 31 |
const messages = []; |
| 32 |
let foundUserMessage = false; |
| 33 |
for (const { type, details } of get().messagesRaw.toReversed()) { |
| 34 |
const finished = |
| 35 |
['completed', 'canceled'].includes(details.status) || |
| 36 |
(['status'].includes(type) && details.type === 'workflow-canceled'); |
| 37 |
if (type === 'workflow' && finished) break; |
| 38 |
if (type === 'workflow-component' && finished) break; |
| 39 |
// This prevents a loop of assistant messages from being at the end |
| 40 |
if (type === 'message' && details.role === 'user') { |
| 41 |
foundUserMessage = true; |
| 42 |
} |
| 43 |
if (type === 'message' && !foundUserMessage) continue; |
| 44 |
if (type === 'message') messages.push(details); |
| 45 |
} |
| 46 |
return messages.toReversed(); |
| 47 |
}, |
| 48 |
hasMessages: () => get().messages.length > 0, |
| 49 |
addMessage: (type, details) => { |
| 50 |
const id = makeId(); |
| 51 |
// If there's an agent, check if seen before |
| 52 |
if (details?.agent?.name) { |
| 53 |
const seenAgents = get().seenAgents; |
| 54 |
if (!seenAgents.includes(details.agent.name)) { |
| 55 |
details.firstSeen = true; |
| 56 |
const seen = (state) => |
| 57 |
new Set([...state.seenAgents, details.agent.name]); |
| 58 |
set((state) => ({ |
| 59 |
seenAgents: [...seen(state)], |
| 60 |
})); |
| 61 |
} |
| 62 |
} |
| 63 |
set((state) => { |
| 64 |
// max 150 messages |
| 65 |
const max = Math.max(0, state.messages.length - 149); |
| 66 |
const next = { id, type, details }; |
| 67 |
return { |
| 68 |
// { id: 1, type: message, details: { role: 'user', content: 'Hello' } } |
| 69 |
// { id: 2, type: message, details: { role: 'assistant', content: 'Hi there!' } } |
| 70 |
// { id: 3, type: workflow, details: { name: 'Workflow 1' } } |
| 71 |
// { id: 5, type: status, details: { type: 'calling-agent' } |
| 72 |
messages: [...state.messages.toSpliced(0, max), next], |
| 73 |
messagesRaw: [...state.messagesRaw.toSpliced(0, max), next], |
| 74 |
}; |
| 75 |
}); |
| 76 |
return id; |
| 77 |
}, |
| 78 |
clearMessages: () => set({ messages: [] }), |
| 79 |
}); |
| 80 |
|
| 81 |
const path = '/extendify/v1/agent/chat-events'; |
| 82 |
const storage = { |
| 83 |
getItem: async () => await apiFetch({ path }), |
| 84 |
setItem: async (_name, state) => |
| 85 |
await apiFetch({ path, method: 'POST', data: { state } }), |
| 86 |
}; |
| 87 |
|
| 88 |
export const useChatStore = create()( |
| 89 |
persist(devtools(state, { name: 'Extendify Agent Chat' }), { |
| 90 |
name: 'extendify-agent-chat', |
| 91 |
storage: createJSONStorage(() => storage), |
| 92 |
skipHydration: true, |
| 93 |
}), |
| 94 |
); |
| 95 |
|