Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions custom/ChatSurface.vue
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
</div>
</div>
<Button
v-if="!agentStore.isResponseInProgress"
class="absolute right-4 bottom-2 !p-0 h-9 w-9"
@click="sendMessage"
:disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
Expand All @@ -179,6 +180,15 @@
text-white"
/>
</Button>
<Button
v-else
class="absolute right-4 bottom-2 !p-0 h-9 w-9"
@click="stopCurrentRequest"
>
<div
class="w-3 h-3 bg-white rounded-sm"
/>
</Button>
</div>
</div>
</div>
Expand Down Expand Up @@ -231,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();
Expand Down Expand Up @@ -316,6 +327,10 @@ async function sendMessage() {
conversationArea.value?.handleSendMessage();
}

function stopCurrentRequest() {
agentStore.abortCurrentChatRequestAndAddSystemMessage();
}

function updateHeight() {
dvh.value = Math.round(window.visualViewport?.height || window.innerHeight);
}
Expand Down
12 changes: 12 additions & 0 deletions custom/composables/agentStore/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions custom/composables/agentStore/pageContext.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
69 changes: 69 additions & 0 deletions custom/composables/agentStore/useAgentChat.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
activeModeName: Ref<string | null>;
};

export function createAgentChatManager({
lastMessage,
activeModeName,
}: CreateAgentChatManagerOptions) {
const chats = new Map<string, Chat<any>>();
const currentChat = shallowRef<Chat<any> | 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,
};
}
142 changes: 142 additions & 0 deletions custom/composables/agentStore/useAgentPlaceholder.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>;
};

export function createAgentPlaceholderController({
userMessageInput,
}: CreateAgentPlaceholderControllerOptions) {
const userMessagePlaceholder = ref(DEFAULT_TEXTAREA_PLACEHOLDER);
const placeholderMessages = ref<string[]>([]);
const hasTypedMessageInPageSession = ref(false);

let placeholderAnimationTimer: ReturnType<typeof setTimeout> | 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,
};
}
Loading