From 7803dd0e374a24c62de1c8f29d1b741ed2cd2841 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 4 May 2026 11:16:55 +0300 Subject: [PATCH 1/5] chore: add abort logic progress[1] --- custom/ChatSurface.vue | 14 ++++++++++++++ custom/composables/useAgentStore.ts | 7 ++++++- index.ts | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/custom/ChatSurface.vue b/custom/ChatSurface.vue index f4c817a..cb4aba3 100644 --- a/custom/ChatSurface.vue +++ b/custom/ChatSurface.vue @@ -170,6 +170,7 @@ + @@ -316,6 +326,10 @@ async function sendMessage() { conversationArea.value?.handleSendMessage(); } +function stopCurrentRequest() { + agentStore.abortCurrentChatRequest(); +} + function updateHeight() { dvh.value = Math.round(window.visualViewport?.height || window.innerHeight); } diff --git a/custom/composables/useAgentStore.ts b/custom/composables/useAgentStore.ts index 0412f77..85fdd3c 100644 --- a/custom/composables/useAgentStore.ts +++ b/custom/composables/useAgentStore.ts @@ -252,6 +252,10 @@ export const useAgentStore = defineStore('agent', () => { } + function abortCurrentChatRequest() { + currentChat.value?.stop(); + } + function clearPlaceholderAnimationTimer() { if (placeholderAnimationTimer !== null) { clearTimeout(placeholderAnimationTimer); @@ -638,6 +642,7 @@ export const useAgentStore = defineStore('agent', () => { MAX_WIDTH, MIN_WIDTH, getLocalStorageItem, - addDebugMessage + addDebugMessage, + abortCurrentChatRequest } }) diff --git a/index.ts b/index.ts index bc8e588..ceca67e 100644 --- a/index.ts +++ b/index.ts @@ -420,7 +420,7 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin { server.endpoint({ method: 'POST', path: `/agent/response`, - handler: async ({ body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_res }) => { + handler: async ({ body, query, headers, cookies, adminUser, response, requestUrl, _raw_express_res, abortSignal }) => { const res = _raw_express_res; const messageId = randomUUID(); const prompt = body.message; From c5aa52e6e3900cd6412d221d4c0366b302842c6c Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 4 May 2026 11:35:33 +0300 Subject: [PATCH 2/5] refactor: move useAgentStore file logic to a separete files --- custom/composables/agentStore/constants.ts | 12 + custom/composables/agentStore/pageContext.ts | 8 + custom/composables/agentStore/useAgentChat.ts | 69 +++ .../agentStore/useAgentPlaceholder.ts | 142 ++++++ .../agentStore/useAgentSessions.ts | 245 ++++++++++ custom/composables/useAgentStore.ts | 447 +++--------------- custom/env.d.ts | 7 + 7 files changed, 538 insertions(+), 392 deletions(-) create mode 100644 custom/composables/agentStore/constants.ts create mode 100644 custom/composables/agentStore/pageContext.ts create mode 100644 custom/composables/agentStore/useAgentChat.ts create mode 100644 custom/composables/agentStore/useAgentPlaceholder.ts create mode 100644 custom/composables/agentStore/useAgentSessions.ts create mode 100644 custom/env.d.ts diff --git a/custom/composables/agentStore/constants.ts b/custom/composables/agentStore/constants.ts new file mode 100644 index 0000000..a2a4953 --- /dev/null +++ b/custom/composables/agentStore/constants.ts @@ -0,0 +1,12 @@ +export type AgentMode = { + name: string; +}; + +export const DEFAULT_CHAT_WIDTH = 30; +export const MAX_WIDTH = 60; +export const MIN_WIDTH = 25; + +export const DEFAULT_TEXTAREA_PLACEHOLDER = 'Type a message...'; +export const PLACEHOLDER_TYPING_DELAY_MS = 60; +export const PLACEHOLDER_DELETING_DELAY_MS = 35; +export const PLACEHOLDER_HOLD_DELAY_MS = 3000; \ No newline at end of file diff --git a/custom/composables/agentStore/pageContext.ts b/custom/composables/agentStore/pageContext.ts new file mode 100644 index 0000000..a3e9eac --- /dev/null +++ b/custom/composables/agentStore/pageContext.ts @@ -0,0 +1,8 @@ +export function getCurrentPageContext() { + return { + path: window.location.pathname, + fullPath: `${window.location.pathname}${window.location.search}${window.location.hash}`, + title: document.title, + url: window.location.href, + }; +} \ No newline at end of file diff --git a/custom/composables/agentStore/useAgentChat.ts b/custom/composables/agentStore/useAgentChat.ts new file mode 100644 index 0000000..ba04374 --- /dev/null +++ b/custom/composables/agentStore/useAgentChat.ts @@ -0,0 +1,69 @@ +import { DefaultChatTransport } from 'ai'; +import { shallowRef, type Ref } from 'vue'; +import { Chat } from '../../chat'; +import { getCurrentPageContext } from './pageContext'; + +type AgentImportMeta = ImportMeta & { + env: { + VITE_ADMINFORTH_PUBLIC_PATH?: string; + }; +}; + +type CreateAgentChatManagerOptions = { + lastMessage: Ref; + activeModeName: Ref; +}; + +export function createAgentChatManager({ + lastMessage, + activeModeName, +}: CreateAgentChatManagerOptions) { + const chats = new Map>(); + const currentChat = shallowRef | null>(); + + function setCurrentChat(sessionId: string) { + if (chats.has(sessionId)) { + currentChat.value = chats.get(sessionId) || null; + } else { + const newChat = new Chat({ + transport: new DefaultChatTransport({ + api: `${(import.meta as AgentImportMeta).env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/response`, + credentials: 'include', + prepareSendMessagesRequest({ messages }: any) { + const message = lastMessage.value; + const body = { + message, + sessionId, + timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, + mode: activeModeName.value, + currentPage: getCurrentPageContext(), + }; + + return { + headers: { + Accept: 'text/event-stream', + 'x-vercel-ai-ui-message-stream': 'v1', + }, + body + }; + } + }), + onError(error: unknown) { + console.error('Chat error:', error); + }, + }); + chats.set(sessionId, newChat); + currentChat.value = newChat; + } + } + + function abortCurrentChatRequest() { + currentChat.value?.stop(); + } + + return { + currentChat, + setCurrentChat, + abortCurrentChatRequest, + }; +} \ No newline at end of file diff --git a/custom/composables/agentStore/useAgentPlaceholder.ts b/custom/composables/agentStore/useAgentPlaceholder.ts new file mode 100644 index 0000000..76c011a --- /dev/null +++ b/custom/composables/agentStore/useAgentPlaceholder.ts @@ -0,0 +1,142 @@ +import { ref, watch, type Ref } from 'vue'; +import { callAdminForthApi } from '@/utils'; +import { + DEFAULT_TEXTAREA_PLACEHOLDER, + PLACEHOLDER_DELETING_DELAY_MS, + PLACEHOLDER_HOLD_DELAY_MS, + PLACEHOLDER_TYPING_DELAY_MS, +} from './constants'; + +type CreateAgentPlaceholderControllerOptions = { + userMessageInput: Ref; +}; + +export function createAgentPlaceholderController({ + userMessageInput, +}: CreateAgentPlaceholderControllerOptions) { + const userMessagePlaceholder = ref(DEFAULT_TEXTAREA_PLACEHOLDER); + const placeholderMessages = ref([]); + const hasTypedMessageInPageSession = ref(false); + + let placeholderAnimationTimer: ReturnType | null = null; + + function clearPlaceholderAnimationTimer() { + if (placeholderAnimationTimer !== null) { + clearTimeout(placeholderAnimationTimer); + placeholderAnimationTimer = null; + } + } + + function resetPlaceholder() { + clearPlaceholderAnimationTimer(); + userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER; + } + + function stopPlaceholderAnimation() { + resetPlaceholder(); + } + + function startPlaceholderAnimation(messages: string[]) { + clearPlaceholderAnimationTimer(); + + if (!messages.length) { + userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER; + return; + } + + let messageIndex = 0; + let visibleLength = 0; + let isDeleting = false; + + const animate = () => { + const currentMessage = messages[messageIndex]; + + if (!currentMessage) { + resetPlaceholder(); + return; + } + + if (!isDeleting) { + visibleLength += 1; + userMessagePlaceholder.value = currentMessage.slice(0, visibleLength); + + if (visibleLength >= currentMessage.length) { + isDeleting = true; + placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_HOLD_DELAY_MS); + return; + } + + placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS); + return; + } + + visibleLength -= 1; + userMessagePlaceholder.value = currentMessage.slice(0, Math.max(visibleLength, 0)); + + if (visibleLength <= 0) { + isDeleting = false; + messageIndex = (messageIndex + 1) % messages.length; + placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS); + return; + } + + placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_DELETING_DELAY_MS); + }; + + animate(); + } + + async function fetchPlaceholderMessages() { + if (hasTypedMessageInPageSession.value) { + stopPlaceholderAnimation(); + return; + } + + try { + const res = await callAdminForthApi({ + method: 'POST', + path: '/agent/get-placeholder-messages', + }); + + if (res.error) { + console.error('Error fetching placeholder messages:', res.error); + placeholderMessages.value = []; + resetPlaceholder(); + return; + } + + placeholderMessages.value = Array.isArray(res.messages) + ? res.messages.filter((message: unknown): message is string => typeof message === 'string' && message.length > 0) + : []; + + if (!placeholderMessages.value.length) { + resetPlaceholder(); + return; + } + + startPlaceholderAnimation(placeholderMessages.value); + } catch (error) { + console.error('Error fetching placeholder messages', error); + placeholderMessages.value = []; + resetPlaceholder(); + } + } + + watch(userMessageInput, (newVal: unknown) => { + if (hasTypedMessageInPageSession.value) { + return; + } + + if (typeof newVal === 'string' && newVal.trim() !== '') { + hasTypedMessageInPageSession.value = true; + stopPlaceholderAnimation(); + } + }); + + return { + userMessagePlaceholder, + hasTypedMessageInPageSession, + fetchPlaceholderMessages, + stopPlaceholderAnimation, + }; +} \ No newline at end of file diff --git a/custom/composables/agentStore/useAgentSessions.ts b/custom/composables/agentStore/useAgentSessions.ts new file mode 100644 index 0000000..a41d695 --- /dev/null +++ b/custom/composables/agentStore/useAgentSessions.ts @@ -0,0 +1,245 @@ +import type { ComputedRef, Ref, ShallowRef } from 'vue'; +import { callAdminForthApi } from '@/utils'; +import type { Chat } from '../../chat'; +import type { IAgentSession, ISessionsListItem, IPart } from '../../types'; + +type AdminforthLike = { + confirm(options: { message: string; yes: string; no: string }): Promise; +}; + +type CreateAgentSessionManagerOptions = { + activeSessionId: Ref; + currentSession: Ref; + sessionList: Ref; + sessions: Ref>; + currentChat: ShallowRef | null | undefined>; + trimmedUserMessage: ComputedRef; + isResponseInProgress: ComputedRef; + userMessageInput: Ref; + lastMessage: Ref; + blockCloseOfChat: Ref; + adminforth: AdminforthLike; + setCurrentChat: (sessionId: string) => void; +}; + +export function createAgentSessionManager({ + activeSessionId, + currentSession, + sessionList, + sessions, + currentChat, + trimmedUserMessage, + isResponseInProgress, + userMessageInput, + lastMessage, + blockCloseOfChat, + adminforth, + setCurrentChat, +}: CreateAgentSessionManagerOptions) { + function sortSessionsListByTimestamp(sessionsListToSort: ISessionsListItem[]) { + return [...sessionsListToSort].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp)); + } + + function saveCurrentSessionInCache() { + if (currentSession.value) { + currentSession.value.messages = currentChat.value?.messages.map((m: any) => ({ + role: m.role, + text: m.parts.map((p: IPart) => p.type === 'text' ? p.text : '').join(''), + })) || []; + sessions.value[currentSession.value.sessionId] = currentSession.value; + } + } + + async function fetchSession(sessionId: string) { + try { + const res = await callAdminForthApi({ + method: 'POST', + path: '/agent/get-session-info', + body: { sessionId }, + }); + if (res.error) { + console.error('Error fetching session:', res.error); + return; + } + sessions.value[sessionId] = res.session; + setCurrentChat(sessionId); + } catch (error) { + console.error('Error fetching session', error); + } + } + + async function setActiveSession(sessionId: string) { + activeSessionId.value = sessionId; + saveCurrentSessionInCache(); + if (!sessions.value[sessionId]) { + await fetchSession(sessionId); + } + currentSession.value = sessions.value[sessionId]; + setCurrentChat(sessionId); + if (currentChat.value.messages.length === 0) { + currentChat.value.messages = currentSession.value?.messages.map((m: any) => ({ + role: m.role, + parts:[{ + type: 'text', + text: m.text, + state: 'done', + }] + })); + } + } + + async function deletePreSession() { + sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== 'pre-session'); + if (activeSessionId.value === 'pre-session') { + activeSessionId.value = null; + currentSession.value = null; + } + } + + async function createNewSession(triggerMessage?: string) { + try { + const res = await callAdminForthApi({ + method: 'POST', + path: '/agent/create-session', + body: { + triggerMessage + }, + }); + if (res.error) { + console.error('Error creating new session:', res.error); + return; + } + deletePreSession(); + sessions.value[res.sessionId] = res; + sessionList.value.unshift({ + sessionId: res.sessionId, + title: res.title, + timestamp: new Date().toISOString(), + }); + setActiveSession(res.sessionId); + } catch (error) { + console.error('Error creating new session', error); + } + } + + async function sendMessage() { + const message = trimmedUserMessage.value; + if (!message || isResponseInProgress.value) { + return; + } + if (!currentSession.value || currentSession.value.sessionId === 'pre-session') { + await createNewSession(message); + } + currentSession.value!.timestamp = new Date().toISOString(); + sessionList.value = sortSessionsListByTimestamp(sessionList.value.map((s: ISessionsListItem) => s.sessionId === currentSession.value?.sessionId ? { + ...s, + timestamp: currentSession.value?.timestamp || s.timestamp, + } : s)); + lastMessage.value = message; + currentChat.value?.sendMessage({ + text: message, + }); + userMessageInput.value = ''; + } + + async function createPreSession() { + saveCurrentSessionInCache(); + if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === 'pre-session')) { + sessionList.value.unshift({ + sessionId: 'pre-session', + title: 'New Session', + timestamp: new Date().toISOString(), + }); + } + + activeSessionId.value = 'pre-session'; + currentSession.value = { + sessionId: 'pre-session', + title: 'New Session', + timestamp: new Date().toISOString(), + messages: [], + }; + sessions.value['pre-session'] = currentSession.value; + setCurrentChat('pre-session'); + } + + async function deleteSession(sessionId: string) { + if (sessionId === 'pre-session') { + deletePreSession(); + return; + } + blockCloseOfChat.value = true; + const isConfirmed = await adminforth.confirm({message: 'Are you sure, that you want to delete this session?', yes: 'Yes', no: 'No'}); + blockCloseOfChat.value = false; + if (!isConfirmed) { + return; + } + try { + const res = await callAdminForthApi({ + method: 'POST', + path: '/agent/delete-session', + body: { sessionId }, + }); + if (res.error) { + console.error('Error deleting session:', res.error); + return; + } + delete sessions.value[sessionId]; + sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== sessionId); + if (activeSessionId.value === sessionId) { + activeSessionId.value = null; + currentSession.value = null; + } + } catch (error) { + console.error('Error deleting session', error); + } + if(sessionId === activeSessionId.value) { + activeSessionId.value = sessionList.value.length > 0 ? sessionList.value[0].sessionId : null; + if (activeSessionId.value) { + currentSession.value = sessions.value[activeSessionId.value] || null; + } else { + currentSession.value = null; + } + } + } + + async function fetchSessionsList() { + try { + const res = await callAdminForthApi({ + method: 'POST', + path: '/agent/get-sessions', + body: { + limit: 100, + }, + }); + if (res.error) { + console.error('Error fetching sessions list:', res.error); + return; + } + sessionList.value = res.sessions; + } catch (error) { + console.error('Error fetching sessions list', error); + } + } + + function addDebugMessage(message: string) { + const debugMessage = { + role: 'assistant', + parts: [{ + type: 'text', + text: message, + state: 'done', + }] + }; + currentChat.value?.messages.push(debugMessage); + } + + return { + sendMessage, + createPreSession, + setActiveSession, + fetchSessionsList, + deleteSession, + addDebugMessage, + }; +} \ No newline at end of file diff --git a/custom/composables/useAgentStore.ts b/custom/composables/useAgentStore.ts index 85fdd3c..9154ad2 100644 --- a/custom/composables/useAgentStore.ts +++ b/custom/composables/useAgentStore.ts @@ -1,37 +1,22 @@ import { defineStore } from 'pinia'; -import { IAgentSession, ISessionsListItem, IMessage, IPart } from '../types'; -import { ref, nextTick, computed, watch, onMounted, shallowRef } from 'vue'; -import { callAdminForthApi } from '@/utils'; +import { IAgentSession, ISessionsListItem } from '../types'; +import { ref, nextTick, computed, watch, onMounted } from 'vue'; import { useAdminforth } from '@/adminforth'; -import { Chat } from '../chat'; -import { DefaultChatTransport } from 'ai'; import { useCoreStore } from '@/stores/core'; import { useAgentTransitions } from './useAgentTransitions'; import { useWindowSize } from '@vueuse/core'; import { remToPx, pxToRem } from '../utils'; - -type AgentMode = { - name: string; -}; - -function getCurrentPageContext() { - return { - path: window.location.pathname, - fullPath: `${window.location.pathname}${window.location.search}${window.location.hash}`, - title: document.title, - url: window.location.href, - }; -} - -const DEFAULT_TEXTAREA_PLACEHOLDER = 'Type a message...'; -const PLACEHOLDER_TYPING_DELAY_MS = 60; -const PLACEHOLDER_DELETING_DELAY_MS = 35; -const PLACEHOLDER_HOLD_DELAY_MS = 3000; +import { + type AgentMode, + DEFAULT_CHAT_WIDTH, + MAX_WIDTH, + MIN_WIDTH, +} from './agentStore/constants'; +import { createAgentChatManager } from './agentStore/useAgentChat'; +import { createAgentPlaceholderController } from './agentStore/useAgentPlaceholder'; +import { createAgentSessionManager } from './agentStore/useAgentSessions'; export const useAgentStore = defineStore('agent', () => { - const DEFAULT_CHAT_WIDTH = 30; - const MAX_WIDTH = 60; - const MIN_WIDTH = 25 const agentTransitions = useAgentTransitions(); const activeSessionId = ref(null); @@ -43,8 +28,6 @@ export const useAgentStore = defineStore('agent', () => { const isSessionHistoryOpen = ref(false); const textInput = ref(null); const userMessageInput = ref(); - const userMessagePlaceholder = ref(DEFAULT_TEXTAREA_PLACEHOLDER); - const placeholderMessages = ref([]); const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : ''); const lastMessage = ref(''); const isTeleportedToBody = ref(false); @@ -58,13 +41,24 @@ export const useAgentStore = defineStore('agent', () => { const chatWidth = ref(DEFAULT_CHAT_WIDTH); const availableModes = ref([]); const activeModeName = ref(null); - const hasTypedMessageInPageSession = ref(false); const { width: windowWidth } = useWindowSize(); - const chats = new Map>(); - const currentChat = shallowRef>(); - - let placeholderAnimationTimer: ReturnType | null = null; + const { + currentChat, + setCurrentChat, + abortCurrentChatRequest, + } = createAgentChatManager({ + lastMessage, + activeModeName, + }); + const { + userMessagePlaceholder, + hasTypedMessageInPageSession, + fetchPlaceholderMessages, + stopPlaceholderAnimation, + } = createAgentPlaceholderController({ + userMessageInput, + }); function setLocalStorageItem(key: string, value: string) { window.localStorage.setItem(`${coreStore.config.brandName || 'adminforth'}-${key}`, value); @@ -72,7 +66,34 @@ export const useAgentStore = defineStore('agent', () => { function getLocalStorageItem(key: string) { return window.localStorage.getItem(`${coreStore.config.brandName || 'adminforth'}-${key}`); } - watch(windowWidth, (newWidth: number) => { + + const isResponseInProgress = computed( () => { + return currentChat.value?.status === 'streaming'; + }); + const blockCloseOfChat = ref(false); + const { + sendMessage, + createPreSession, + setActiveSession, + fetchSessionsList, + deleteSession, + addDebugMessage, + } = createAgentSessionManager({ + activeSessionId, + currentSession, + sessionList, + sessions, + currentChat, + trimmedUserMessage, + isResponseInProgress, + userMessageInput, + lastMessage, + blockCloseOfChat, + adminforth, + setCurrentChat, + }); + + watch(() => windowWidth.value, (newWidth) => { if (isFullScreen.value) { setChatWidth(newWidth, false); } @@ -94,16 +115,6 @@ export const useAgentStore = defineStore('agent', () => { setLocalStorageItem('lastSessionId', newVal); } }) - watch(userMessageInput, (newVal: unknown) => { - if (hasTypedMessageInPageSession.value) { - return; - } - - if (typeof newVal === 'string' && newVal.trim() !== '') { - hasTypedMessageInPageSession.value = true; - stopPlaceholderAnimation(); - } - }) onMounted(() => { const chatWidthBeforeFullScreen = parseInt(getLocalStorageItem('chatWidthBeforeFullScreen') || '0', 10); if (chatWidthBeforeFullScreen && (chatWidthBeforeFullScreen > MAX_WIDTH || chatWidthBeforeFullScreen < MIN_WIDTH)) { @@ -215,142 +226,6 @@ export const useAgentStore = defineStore('agent', () => { activeModeName.value = modeName; } - function setCurrentChat(sessionId: string) { - if (chats.has(sessionId)) { - currentChat.value = chats.get(sessionId) || null; - } else { - const newChat = new Chat({ - transport: new DefaultChatTransport({ - api: `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/response`, - credentials: 'include', - prepareSendMessagesRequest({ messages }: any) { - const message = lastMessage.value; - const body = { - message, - sessionId, - timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, - mode: activeModeName.value, - currentPage: getCurrentPageContext(), - }; - - return { - headers: { - Accept: 'text/event-stream', - 'x-vercel-ai-ui-message-stream': 'v1', - }, - body - }; - } - }), - onError(error: unknown) { - console.error("Chat error:", error); - }, - }); - chats.set(sessionId, newChat); - currentChat.value = newChat; - } - - } - - function abortCurrentChatRequest() { - currentChat.value?.stop(); - } - - function clearPlaceholderAnimationTimer() { - if (placeholderAnimationTimer !== null) { - clearTimeout(placeholderAnimationTimer); - placeholderAnimationTimer = null; - } - } - - function resetPlaceholder() { - clearPlaceholderAnimationTimer(); - userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER; - } - - function stopPlaceholderAnimation() { - resetPlaceholder(); - } - - function startPlaceholderAnimation(messages: string[]) { - clearPlaceholderAnimationTimer(); - - if (!messages.length) { - userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER; - return; - } - - let messageIndex = 0; - let visibleLength = 0; - let isDeleting = false; - - const animate = () => { - const currentMessage = messages[messageIndex]; - - if (!currentMessage) { - resetPlaceholder(); - return; - } - - if (!isDeleting) { - visibleLength += 1; - userMessagePlaceholder.value = currentMessage.slice(0, visibleLength); - - if (visibleLength >= currentMessage.length) { - isDeleting = true; - placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_HOLD_DELAY_MS); - return; - } - - placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS); - return; - } - - visibleLength -= 1; - userMessagePlaceholder.value = currentMessage.slice(0, Math.max(visibleLength, 0)); - - if (visibleLength <= 0) { - isDeleting = false; - messageIndex = (messageIndex + 1) % messages.length; - placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS); - return; - } - - placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_DELETING_DELAY_MS); - }; - - animate(); - } - - const isResponseInProgress = computed( () => { - return currentChat.value?.status === 'streaming'; - }); - const blockCloseOfChat = ref(false); - - function sortSessionsListByTimestamp(sessionsList: ISessionsListItem[]) { - return [...sessionsList].sort((a: ISessionsListItem, b: ISessionsListItem) => b.timestamp.localeCompare(a.timestamp)); - } - - async function sendMessage() { - const message = trimmedUserMessage.value; - if (!message || isResponseInProgress.value) { - return; - } - if (!currentSession.value || currentSession.value.sessionId === 'pre-session') { - await createNewSession(message); - } - currentSession.value!.timestamp = new Date().toISOString(); - sessionList.value = sortSessionsListByTimestamp(sessionList.value.map((s: ISessionsListItem) => s.sessionId === currentSession.value?.sessionId ? { - ...s, - timestamp: currentSession.value?.timestamp || s.timestamp, - } : s)); - lastMessage.value = message; - currentChat.value?.sendMessage({ - text: message, - }); - userMessageInput.value = ''; - } - function closeChat() { if(isFullScreen.value) { document.body.style.overflow = ''; @@ -391,218 +266,6 @@ export const useAgentStore = defineStore('agent', () => { textInput.value = el; } - async function fetchPlaceholderMessages() { - if (hasTypedMessageInPageSession.value) { - stopPlaceholderAnimation(); - return; - } - - try { - const res = await callAdminForthApi({ - method: 'POST', - path: '/agent/get-placeholder-messages', - }); - - if (res.error) { - console.error('Error fetching placeholder messages:', res.error); - placeholderMessages.value = []; - resetPlaceholder(); - return; - } - - placeholderMessages.value = Array.isArray(res.messages) - ? res.messages.filter((message: unknown): message is string => typeof message === 'string' && message.length > 0) - : []; - - if (!placeholderMessages.value.length) { - resetPlaceholder(); - return; - } - - startPlaceholderAnimation(placeholderMessages.value); - } catch (error) { - console.error('Error fetching placeholder messages', error); - placeholderMessages.value = []; - resetPlaceholder(); - } - } - - - //create a pre-session, until user will type something, so we can save session - async function createPreSession() { - saveCurrentSessionInCache(); - if (!sessionList.value.some((s: ISessionsListItem) => s.sessionId === 'pre-session')) { - sessionList.value.unshift({ - sessionId: 'pre-session', - title: 'New Session', - timestamp: new Date().toISOString(), - }); - } - - activeSessionId.value = 'pre-session'; - currentSession.value = { - sessionId: 'pre-session', - title: 'New Session', - timestamp: new Date().toISOString(), - messages: [], - }; - sessions.value['pre-session'] = currentSession.value; - setCurrentChat('pre-session'); - } - - async function deletePreSession() { - sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== 'pre-session'); - if (activeSessionId.value === 'pre-session') { - activeSessionId.value = null; - currentSession.value = null; - } - } - - async function createNewSession(triggerMessage?: string) { - try { - const res = await callAdminForthApi({ - method: 'POST', - path: '/agent/create-session', - body: { - triggerMessage - }, - }); - if (res.error) { - console.error('Error creating new session:', res.error); - return; - } - deletePreSession(); - sessions.value[res.sessionId] = res; - sessionList.value.unshift({ - sessionId: res.sessionId, - title: res.title, - timestamp: new Date().toISOString(), - }); - setActiveSession(res.sessionId); - } catch (error) { - console.error('Error creating new session', error); - } - } - - async function deleteSession(sessionId: string) { - if (sessionId === 'pre-session') { - deletePreSession(); - return; - } - blockCloseOfChat.value = true; - const isConfirmed = await adminforth.confirm({message: 'Are you sure, that you want to delete this session?', yes: 'Yes', no: 'No'}) - blockCloseOfChat.value = false; - if (!isConfirmed) { - return; - } - try { - const res = await callAdminForthApi({ - method: 'POST', - path: '/agent/delete-session', - body: { sessionId }, - }); - if (res.error) { - console.error('Error deleting session:', res.error); - return; - } - delete sessions.value[sessionId]; - sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== sessionId); - if (activeSessionId.value === sessionId) { - activeSessionId.value = null; - currentSession.value = null; - } - } catch (error) { - console.error('Error deleting session', error); - } - if(sessionId === activeSessionId.value) { - activeSessionId.value = sessionList.value.length > 0 ? sessionList.value[0].sessionId : null; - if (activeSessionId.value) { - currentSession.value = sessions.value[activeSessionId.value] || null; - } else { - currentSession.value = null; - } - } - } - - async function fetchSession(sessionId: string) { - try { - const res = await callAdminForthApi({ - method: 'POST', - path: '/agent/get-session-info', - body: { sessionId }, - }); - if (res.error) { - console.error('Error fetching session:', res.error); - return; - } - sessions.value[sessionId] = res.session; - setCurrentChat(sessionId); - } catch (error) { - console.error('Error fetching session', error); - } - } - - function saveCurrentSessionInCache() { - if (currentSession.value) { - currentSession.value.messages = currentChat.value?.messages.map((m: any) => ({ - role: m.role, - text: m.parts.map((p: IPart) => p.type === 'text' ? p.text : '').join(''), - })) || []; - sessions.value[currentSession.value.sessionId] = currentSession.value; - } - } - - async function setActiveSession(sessionId: string) { - activeSessionId.value = sessionId; - saveCurrentSessionInCache(); - if (!sessions.value[sessionId]) { - await fetchSession(sessionId); - } - currentSession.value = sessions.value[sessionId]; - setCurrentChat(sessionId); - if (currentChat.value.messages.length === 0) { - currentChat.value.messages = currentSession.value?.messages.map((m: any) => ({ - role: m.role, - parts:[{ - type: 'text', - text: m.text, - state: 'done', - }] - })); - } - } - - async function fetchSessionsList() { - try { - const res = await callAdminForthApi({ - method: 'POST', - path: '/agent/get-sessions', - body: { - limit: 100, - }, - }); - if (res.error) { - console.error('Error fetching sessions list:', res.error); - return; - } - sessionList.value = res.sessions; - } catch (error) { - console.error('Error fetching sessions list', error); - } - } - - function addDebugMessage(message: string) { - const debugMessage = { - role: 'assistant', - parts: [{ - type: 'text', - text: message, - state: 'done', - }] - }; - currentChat.value?.messages.push(debugMessage); - } - return { //_________-Sessions management-_____________ activeSessionId, diff --git a/custom/env.d.ts b/custom/env.d.ts new file mode 100644 index 0000000..e69e66b --- /dev/null +++ b/custom/env.d.ts @@ -0,0 +1,7 @@ +interface ImportMetaEnv { + readonly VITE_ADMINFORTH_PUBLIC_PATH?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file From 4c1b71b77705987249c5b82c13eb6169f7e1d163 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 4 May 2026 11:47:42 +0300 Subject: [PATCH 3/5] fix: correct save of generation mode in local storage --- custom/ChatSurface.vue | 1 + custom/composables/useAgentStore.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/custom/ChatSurface.vue b/custom/ChatSurface.vue index cb4aba3..55cb8b0 100644 --- a/custom/ChatSurface.vue +++ b/custom/ChatSurface.vue @@ -241,6 +241,7 @@ onClickOutside(modeMenu, () => { isModeMenuOpen.value = false; }); onMounted(async () => { agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName); + agentStore.setCurrentGenerationModeFromLocalStorage(); agentStore.regisrerTextInput(textInput.value); window.addEventListener('resize', updateHeight) textInput.value?.focus(); diff --git a/custom/composables/useAgentStore.ts b/custom/composables/useAgentStore.ts index 9154ad2..1bf24ef 100644 --- a/custom/composables/useAgentStore.ts +++ b/custom/composables/useAgentStore.ts @@ -148,10 +148,6 @@ export const useAgentStore = defineStore('agent', () => { if (coreStore.isMobile) { setChatWidth(window.innerWidth); } - const ativeModeNameFromLocalStorage = getLocalStorageItem('activeModeName'); - if (ativeModeNameFromLocalStorage) { - setActiveMode(ativeModeNameFromLocalStorage); - } appRoot.value = document.getElementById('app'); header.value = document.getElementById('af-header-nav'); if (appRoot.value && header.value) { @@ -218,6 +214,13 @@ export const useAgentStore = defineStore('agent', () => { ?? null; } + function setCurrentGenerationModeFromLocalStorage() { + const activeModeNameFromLocalStorage = getLocalStorageItem('activeModeName'); + if (activeModeNameFromLocalStorage) { + setActiveMode(activeModeNameFromLocalStorage); + } + } + function setActiveMode(modeName: string) { if (!availableModes.value.some((mode: AgentMode) => mode.name === modeName)) { return; @@ -300,12 +303,13 @@ export const useAgentStore = defineStore('agent', () => { availableModes, activeModeName, setAvailableModes, + setCurrentGenerationModeFromLocalStorage, setActiveMode, DEFAULT_CHAT_WIDTH, MAX_WIDTH, MIN_WIDTH, getLocalStorageItem, addDebugMessage, - abortCurrentChatRequest + abortCurrentChatRequest, } }) From 8cb6645a48aa1d9de1a707f916871fd78d6a9e1d Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 4 May 2026 12:26:00 +0300 Subject: [PATCH 4/5] fix: correct save of chat opened state in local storage --- custom/composables/useAgentStore.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/custom/composables/useAgentStore.ts b/custom/composables/useAgentStore.ts index 1bf24ef..ff5d0ca 100644 --- a/custom/composables/useAgentStore.ts +++ b/custom/composables/useAgentStore.ts @@ -133,10 +133,11 @@ export const useAgentStore = defineStore('agent', () => { } if (!coreStore.isMobile) { const savedIsTeleportedToBody = getLocalStorageItem('isTeleportedToBody'); + const savedIsTeleportedToBodyBeforeFullScreen = getLocalStorageItem('isTeleportedToBodyBeforeFullScreen'); + const isTeleportedToBodyFromLocalStorage = savedIsTeleportedToBody === 'true' || savedIsTeleportedToBodyBeforeFullScreen === 'true'; const savedIsChatOpen = getLocalStorageItem('isChatOpen'); - const shouldTeleportToBody = savedIsTeleportedToBody === null ? true : savedIsTeleportedToBody === 'true'; - setIsTeleportedToBody(shouldTeleportToBody); + setIsTeleportedToBody(isTeleportedToBodyFromLocalStorage); if (isTeleportedToBody.value) { isChatOpen.value = savedIsChatOpen === null ? true : savedIsChatOpen === 'true'; } From 9996cb23c2c11dc6b86c3737533a14b9c652eee8 Mon Sep 17 00:00:00 2001 From: yaroslav8765 Date: Mon, 4 May 2026 13:03:43 +0300 Subject: [PATCH 5/5] feat: add abilyty to stop generation(frontend implementation) --- custom/ChatSurface.vue | 2 +- .../agentStore/useAgentSessions.ts | 25 +++++++++++++++++++ custom/composables/useAgentStore.ts | 9 ++++++- .../conversation_area/ProcessingTimeline.vue | 2 +- index.ts | 12 +++++++++ 5 files changed, 47 insertions(+), 3 deletions(-) diff --git a/custom/ChatSurface.vue b/custom/ChatSurface.vue index 55cb8b0..99b02d7 100644 --- a/custom/ChatSurface.vue +++ b/custom/ChatSurface.vue @@ -328,7 +328,7 @@ async function sendMessage() { } function stopCurrentRequest() { - agentStore.abortCurrentChatRequest(); + agentStore.abortCurrentChatRequestAndAddSystemMessage(); } function updateHeight() { diff --git a/custom/composables/agentStore/useAgentSessions.ts b/custom/composables/agentStore/useAgentSessions.ts index a41d695..1cc287e 100644 --- a/custom/composables/agentStore/useAgentSessions.ts +++ b/custom/composables/agentStore/useAgentSessions.ts @@ -234,6 +234,30 @@ export function createAgentSessionManager({ currentChat.value?.messages.push(debugMessage); } + function addSystemMessage(message: string) { + const systemMessage = { + role: 'system', + parts: [{ + type: 'text', + text: message, + state: 'done', + }] + }; + currentChat.value?.messages.push(systemMessage); + try { + const res = callAdminForthApi({ + method: 'POST', + path: '/agent/add-system-message-to-turns', + body: { + sessionId: activeSessionId.value, + systemMessage: message, + }, + }); + } catch (error) { + console.error('Error adding system message', error); + } + } + return { sendMessage, createPreSession, @@ -241,5 +265,6 @@ export function createAgentSessionManager({ fetchSessionsList, deleteSession, addDebugMessage, + addSystemMessage, }; } \ No newline at end of file diff --git a/custom/composables/useAgentStore.ts b/custom/composables/useAgentStore.ts index ff5d0ca..d886eaa 100644 --- a/custom/composables/useAgentStore.ts +++ b/custom/composables/useAgentStore.ts @@ -78,6 +78,7 @@ export const useAgentStore = defineStore('agent', () => { fetchSessionsList, deleteSession, addDebugMessage, + addSystemMessage, } = createAgentSessionManager({ activeSessionId, currentSession, @@ -270,6 +271,11 @@ export const useAgentStore = defineStore('agent', () => { textInput.value = el; } + function abortCurrentChatRequestAndAddSystemMessage() { + abortCurrentChatRequest(); + addSystemMessage('[Response generation aborted]'); + } + return { //_________-Sessions management-_____________ activeSessionId, @@ -311,6 +317,7 @@ export const useAgentStore = defineStore('agent', () => { MIN_WIDTH, getLocalStorageItem, addDebugMessage, - abortCurrentChatRequest, + abortCurrentChatRequestAndAddSystemMessage, + addSystemMessage } }) diff --git a/custom/conversation_area/ProcessingTimeline.vue b/custom/conversation_area/ProcessingTimeline.vue index 2d03bbf..f02e668 100644 --- a/custom/conversation_area/ProcessingTimeline.vue +++ b/custom/conversation_area/ProcessingTimeline.vue @@ -68,7 +68,7 @@ }); const showFakeThinkingMessage = computed(() => { - if (props.message.parts.length === 0) return true; + if (props.message.parts.length === 0 && props.isLastMessageInChat) return true; return false; }) diff --git a/index.ts b/index.ts index ceca67e..3688983 100644 --- a/index.ts +++ b/index.ts @@ -810,6 +810,18 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin { ok: true }; } + }), + server.endpoint({ + method: 'POST', + path: `/agent/add-system-message-to-turns`, + handler: async ({body, adminUser, _raw_express_req }) => { + const sessionId = body.sessionId; + const systemMessage = body.systemMessage; + await this.createNewTurn(sessionId, systemMessage); + return { + ok: true + } + } }); } }