Skip to content

Commit 448e8bf

Browse files
committed
2 parents aaae8b9 + 0463373 commit 448e8bf

6 files changed

Lines changed: 185 additions & 8 deletions

File tree

custom/ChatSurface.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
'min-h-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none',
112112
agentStore.availableModes.length > 1 ? 'p-4 pr-12 pb-12' : 'p-4 pr-12',
113113
]"
114-
placeholder="Type a message..."
114+
:placeholder="agentStore.userMessagePlaceholder"
115115
@keydown.enter.exact.prevent="sendMessage"
116116
/>
117117
<div

custom/ConversationArea.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
<script setup lang="ts">
7575
import Message from './Message.vue';
7676
import type { IMessage, IPart } from './types';
77-
import { useTemplateRef, ref, defineAsyncComponent, onMounted, watch, computed } from 'vue';
77+
import { useTemplateRef, ref, defineAsyncComponent, onMounted, onUnmounted, watch, computed } from 'vue';
7878
import { IconArrowDownOutline } from '@iconify-prerendered/vue-flowbite';
7979
import SessionsHistory from './SessionsHistory.vue';
8080
import { useAgentStore } from './composables/useAgentStore';
@@ -98,6 +98,11 @@ function recalculateScroll() {
9898
9999
onMounted(async () => {
100100
await import('@incremark/theme/styles.css')
101+
await agentStore.fetchPlaceholderMessages()
102+
});
103+
104+
onUnmounted(() => {
105+
agentStore.stopPlaceholderAnimation();
101106
});
102107
103108
watch(scrollContainer, () => {

custom/Message.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@
5757
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
5858
import { useRouter } from 'vue-router';
5959
import { IconAngleDownOutline } from '@iconify-prerendered/vue-flowbite';
60-
60+
import { useAgentStore } from './composables/useAgentStore';
6161
const IncremarkContent = defineAsyncComponent(() => import('@incremark/vue').then(module => module.IncremarkContent))
6262
const ShikiCodeBlock = defineAsyncComponent(() => import('./incremark_code_renderers/IncremarkShikiCodeBlock.vue'))
6363
64+
const agentStore = useAgentStore();
65+
6466
const incremarkComponents = {
6567
code: ShikiCodeBlock,
6668
};
@@ -132,6 +134,9 @@
132134
133135
const internalRoute = resolveInternalRoute(href);
134136
if (internalRoute !== null) {
137+
if (agentStore.isFullScreen) {
138+
agentStore.setFullScreen(false);
139+
}
135140
void router.push(internalRoute);
136141
return;
137142
}

custom/composables/useAgentStore.ts

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ type AgentMode = {
1212
name: string;
1313
};
1414

15+
const DEFAULT_TEXTAREA_PLACEHOLDER = 'Type a message...';
16+
const PLACEHOLDER_TYPING_DELAY_MS = 60;
17+
const PLACEHOLDER_DELETING_DELAY_MS = 35;
18+
const PLACEHOLDER_HOLD_DELAY_MS = 3000;
19+
1520
export const useAgentStore = defineStore('agent', () => {
1621
const DEFAULT_CHAT_WIDTH = 600;
1722
const MAX_WIDTH = 800;
@@ -25,8 +30,10 @@ export const useAgentStore = defineStore('agent', () => {
2530
const adminforth = useAdminforth();
2631
const isChatOpen = ref(false);
2732
const isSessionHistoryOpen = ref(false);
28-
const textInput = ref<HTMLInputElement | null>(null);
33+
const textInput = ref<HTMLTextAreaElement | null>(null);
2934
const userMessageInput = ref();
35+
const userMessagePlaceholder = ref(DEFAULT_TEXTAREA_PLACEHOLDER);
36+
const placeholderMessages = ref<string[]>([]);
3037
const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
3138
const lastMessage = ref('');
3239
const isTeleportedToBody = ref(false);
@@ -40,6 +47,9 @@ export const useAgentStore = defineStore('agent', () => {
4047
const chatWidth = ref(DEFAULT_CHAT_WIDTH);
4148
const availableModes = ref<AgentMode[]>([]);
4249
const activeModeName = ref<string | null>(null);
50+
const hasTypedMessageInPageSession = ref(false);
51+
let placeholderAnimationTimer: ReturnType<typeof setTimeout> | null = null;
52+
4353
function setLocalStorageItem(key: string, value: string) {
4454
window.localStorage.setItem(`${coreStore.config.brandName || 'adminforth'}-${key}`, value);
4555
}
@@ -60,6 +70,16 @@ export const useAgentStore = defineStore('agent', () => {
6070
setLocalStorageItem('lastSessionId', newVal);
6171
}
6272
})
73+
watch(userMessageInput, (newVal: unknown) => {
74+
if (hasTypedMessageInPageSession.value) {
75+
return;
76+
}
77+
78+
if (typeof newVal === 'string' && newVal.trim() !== '') {
79+
hasTypedMessageInPageSession.value = true;
80+
stopPlaceholderAnimation();
81+
}
82+
})
6383
onMounted(() => {
6484
const chatWidthBeforeFullScreen = parseInt(getLocalStorageItem('chatWidthBeforeFullScreen') || '0', 10);
6585
if (chatWidthBeforeFullScreen) {
@@ -130,14 +150,14 @@ export const useAgentStore = defineStore('agent', () => {
130150
function setAvailableModes(modes: AgentMode[], defaultModeName?: string | null) {
131151
availableModes.value = modes;
132152
activeModeName.value =
133-
modes.find((mode) => mode.name === activeModeName.value)?.name
153+
modes.find((mode: AgentMode) => mode.name === activeModeName.value)?.name
134154
?? defaultModeName
135155
?? modes[0]?.name
136156
?? null;
137157
}
138158

139159
function setActiveMode(modeName: string) {
140-
if (!availableModes.value.some((mode) => mode.name === modeName)) {
160+
if (!availableModes.value.some((mode: AgentMode) => mode.name === modeName)) {
141161
return;
142162
}
143163

@@ -179,6 +199,73 @@ export const useAgentStore = defineStore('agent', () => {
179199
}
180200

181201
}
202+
203+
function clearPlaceholderAnimationTimer() {
204+
if (placeholderAnimationTimer !== null) {
205+
clearTimeout(placeholderAnimationTimer);
206+
placeholderAnimationTimer = null;
207+
}
208+
}
209+
210+
function resetPlaceholder() {
211+
clearPlaceholderAnimationTimer();
212+
userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER;
213+
}
214+
215+
function stopPlaceholderAnimation() {
216+
resetPlaceholder();
217+
}
218+
219+
function startPlaceholderAnimation(messages: string[]) {
220+
clearPlaceholderAnimationTimer();
221+
222+
if (!messages.length) {
223+
userMessagePlaceholder.value = DEFAULT_TEXTAREA_PLACEHOLDER;
224+
return;
225+
}
226+
227+
let messageIndex = 0;
228+
let visibleLength = 0;
229+
let isDeleting = false;
230+
231+
const animate = () => {
232+
const currentMessage = messages[messageIndex];
233+
234+
if (!currentMessage) {
235+
resetPlaceholder();
236+
return;
237+
}
238+
239+
if (!isDeleting) {
240+
visibleLength += 1;
241+
userMessagePlaceholder.value = currentMessage.slice(0, visibleLength);
242+
243+
if (visibleLength >= currentMessage.length) {
244+
isDeleting = true;
245+
placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_HOLD_DELAY_MS);
246+
return;
247+
}
248+
249+
placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS);
250+
return;
251+
}
252+
253+
visibleLength -= 1;
254+
userMessagePlaceholder.value = currentMessage.slice(0, Math.max(visibleLength, 0));
255+
256+
if (visibleLength <= 0) {
257+
isDeleting = false;
258+
messageIndex = (messageIndex + 1) % messages.length;
259+
placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_TYPING_DELAY_MS);
260+
return;
261+
}
262+
263+
placeholderAnimationTimer = setTimeout(animate, PLACEHOLDER_DELETING_DELAY_MS);
264+
};
265+
266+
animate();
267+
}
268+
182269
const isResponseInProgress = computed( () => {
183270
return currentChat.value?.status === 'streaming';
184271
});
@@ -234,10 +321,46 @@ export const useAgentStore = defineStore('agent', () => {
234321
function setSessionHistoryOpen(isOpen: boolean) {
235322
isSessionHistoryOpen.value = isOpen;
236323
}
237-
function regisrerTextInput(el: HTMLInputElement | null) {
324+
function regisrerTextInput(el: HTMLTextAreaElement | null) {
238325
textInput.value = el;
239326
}
240327

328+
async function fetchPlaceholderMessages() {
329+
if (hasTypedMessageInPageSession.value) {
330+
stopPlaceholderAnimation();
331+
return;
332+
}
333+
334+
try {
335+
const res = await callAdminForthApi({
336+
method: 'POST',
337+
path: '/agent/get-placeholder-messages',
338+
});
339+
340+
if (res.error) {
341+
console.error('Error fetching placeholder messages:', res.error);
342+
placeholderMessages.value = [];
343+
resetPlaceholder();
344+
return;
345+
}
346+
347+
placeholderMessages.value = Array.isArray(res.messages)
348+
? res.messages.filter((message: unknown): message is string => typeof message === 'string' && message.length > 0)
349+
: [];
350+
351+
if (!placeholderMessages.value.length) {
352+
resetPlaceholder();
353+
return;
354+
}
355+
356+
startPlaceholderAnimation(placeholderMessages.value);
357+
} catch (error) {
358+
console.error('Error fetching placeholder messages', error);
359+
placeholderMessages.value = [];
360+
resetPlaceholder();
361+
}
362+
}
363+
241364

242365
//create a pre-session, until user will type something, so we can save session
243366
async function createPreSession() {
@@ -409,12 +532,15 @@ export const useAgentStore = defineStore('agent', () => {
409532
createPreSession,
410533
//____________________________________________
411534
regisrerTextInput,
535+
fetchPlaceholderMessages,
536+
stopPlaceholderAnimation,
412537
isChatOpen,
413538
setIsChatOpen,
414539
isSessionHistoryOpen,
415540
setSessionHistoryOpen,
416541
sendMessage,
417542
userMessageInput,
543+
userMessagePlaceholder,
418544
chatMessages: computed(() => currentChat.value?.messages || []),
419545
trimmedUserMessage,
420546
isResponseInProgress,

index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,33 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
152152
}
153153

154154
setupEndpoints(server: IHttpServer) {
155+
server.endpoint({
156+
method: 'POST',
157+
path: `/agent/get-placeholder-messages`,
158+
handler: async ({ body, query, headers, cookies, adminUser, response, requestUrl }) => {
159+
if (!this.options.placeholderMessages) {
160+
return {
161+
messages: [],
162+
};
163+
}
164+
165+
const messages = await this.options.placeholderMessages({
166+
adminUser,
167+
httpExtra: {
168+
body,
169+
query,
170+
headers,
171+
cookies,
172+
requestUrl,
173+
response,
174+
},
175+
});
176+
177+
return {
178+
messages,
179+
};
180+
}
181+
});
155182
server.endpoint({
156183
method: 'POST',
157184
path: `/agent/response`,

types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { type PluginsCommonOptions, type CompletionAdapter } from "adminforth";
1+
import {
2+
type PluginsCommonOptions,
3+
type CompletionAdapter,
4+
type AdminUser,
5+
type HttpExtra,
6+
} from "adminforth";
27

38
interface ISessionResource {
49
resourceId: string;
@@ -20,6 +25,15 @@ interface ITurnResource {
2025
}
2126

2227
export interface PluginOptions extends PluginsCommonOptions {
28+
/**
29+
* Optional placeholder examples to preload for the chat textarea.
30+
* They are resolved once when the chat frontend loads.
31+
*/
32+
placeholderMessages?: ((input: {
33+
adminUser: AdminUser;
34+
httpExtra: HttpExtra;
35+
}) => string[] | Promise<string[]>);
36+
2337
/**
2438
* Modes for the plugin.
2539
* Each mode can have its own configuration.

0 commit comments

Comments
 (0)