mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
fix ai message + add typer
This commit is contained in:
36
dashboard/composables/useTextType.ts
Normal file
36
dashboard/composables/useTextType.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
|
||||
|
||||
export function useTextType(options: { ms: number, increase: number }, onTickAction?: () => any) {
|
||||
|
||||
let interval: any;
|
||||
const index = ref<number>(0);
|
||||
|
||||
function onTick() {
|
||||
index.value += options.increase;
|
||||
onTickAction?.();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (interval) clearInterval(interval);
|
||||
}
|
||||
|
||||
function resume() {
|
||||
if (interval) clearInterval(interval);
|
||||
interval = setInterval(() => onTick(), options.ms);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (interval) clearTimeout(interval);
|
||||
}
|
||||
|
||||
function start() {
|
||||
index.value = 0;
|
||||
if (interval) clearInterval(interval);
|
||||
interval = setInterval(() => onTick(), options.ms);
|
||||
}
|
||||
|
||||
return { start, stop, resume, pause, index, interval }
|
||||
|
||||
}
|
||||
@@ -26,13 +26,28 @@ const loading = ref<boolean>(false);
|
||||
|
||||
const currentChatId = ref<string>("");
|
||||
const currentChatMessages = ref<{ role: string, content: string, charts?: any[], tool_calls?: any }[]>([]);
|
||||
const currentChatMessageDelta = ref<string>('');
|
||||
const currentChatMessageDelta = ref<string>("");
|
||||
|
||||
const currentChatMessageDeltaHtml = computed(() => {
|
||||
const lastData = currentChatMessageDelta.value.match(/\[(data:(.*?))\]/g);
|
||||
|
||||
const typer = useTextType({ ms: 10, increase: 2 }, () => {
|
||||
const cleanMessage = currentChatMessageDelta.value.replace(/\[(data:(.*?))\]/g, '');
|
||||
if (!lastData || lastData.length == 0) return cleanMessage;
|
||||
return `<div class="flex items-center gap-1"> <i class="fas fa-loader animate-spin"></i> <div> ${lastData.at(-1)}</div> </div> <div> ${cleanMessage} </div>`;
|
||||
if (typer.index.value >= cleanMessage.length) typer.pause();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
typer.stop();
|
||||
})
|
||||
|
||||
const currentChatMessageDeltaTextVisible = computed(() => {
|
||||
const cleanMessage = currentChatMessageDelta.value.replace(/\[(data:(.*?))\]/g, '');
|
||||
const textVisible = cleanMessage.substring(0, typer.index.value);
|
||||
setTimeout(() => scrollToBottom(), 1);
|
||||
return textVisible;
|
||||
});
|
||||
|
||||
const currentChatMessageDeltaShowLoader = computed(() => {
|
||||
const lastData = currentChatMessageDelta.value.match(/\[(data:(.*?))\]$/);
|
||||
return lastData != null;
|
||||
});
|
||||
|
||||
const scroller = ref<HTMLDivElement | null>(null);
|
||||
@@ -51,10 +66,16 @@ async function pollSendMessageStatus(chat_id: string, times: number, updateStatu
|
||||
|
||||
updateStatus(res.status);
|
||||
|
||||
|
||||
typer.resume();
|
||||
|
||||
|
||||
if (res.completed === false) {
|
||||
setTimeout(() => pollSendMessageStatus(chat_id, times + 1, updateStatus), (times > 20 ? 1000 : 500));
|
||||
setTimeout(() => pollSendMessageStatus(chat_id, times + 1, updateStatus), (times > 10 ? 2000 : 1000));
|
||||
} else {
|
||||
|
||||
typer.stop();
|
||||
|
||||
const messages = await $fetch(`/api/ai/${chat_id}/get_messages`, {
|
||||
headers: useComputedHeaders({ useSnapshotDates: false }).value
|
||||
});
|
||||
@@ -62,18 +83,13 @@ async function pollSendMessageStatus(chat_id: string, times: number, updateStatu
|
||||
|
||||
currentChatMessages.value = messages.map(e => ({ ...e, charts: e.charts.map(k => JSON.parse(k)) })) as any;
|
||||
currentChatMessageDelta.value = '';
|
||||
|
||||
// currentChatMessages.value.push({
|
||||
// role: 'assistant',
|
||||
// content: currentChatMessageDelta.value.replace(/\[data:.*?\]/g, ''),
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
|
||||
|
||||
if (loading.value) return;
|
||||
if (!project.value) return;
|
||||
|
||||
@@ -100,14 +116,15 @@ async function sendMessage() {
|
||||
|
||||
await new Promise(e => setTimeout(e, 200));
|
||||
|
||||
|
||||
typer.start();
|
||||
|
||||
await pollSendMessageStatus(res.chat_id, 0, status => {
|
||||
if (!status) return;
|
||||
if (status.length > 0) loading.value = false;
|
||||
currentChatMessageDelta.value = status;
|
||||
});
|
||||
|
||||
|
||||
|
||||
} catch (ex: any) {
|
||||
|
||||
if (ex.message.includes('CHAT_LIMIT_REACHED')) {
|
||||
@@ -237,14 +254,14 @@ async function clearAllChats() {
|
||||
|
||||
<div class="flex w-full flex-col" v-for="(message, messageIndex) of currentChatMessages">
|
||||
|
||||
<div class="flex justify-end w-full poppins text-[1.1rem]" v-if="message.role === 'user'">
|
||||
<div v-if="message.role === 'user'" class="flex justify-end w-full poppins text-[1.1rem]">
|
||||
<div class="bg-lyx-widget-light px-5 py-3 rounded-lg">
|
||||
{{ message.content }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
||||
v-if="message.role === 'assistant' && (debugModeAi ? true : message.content)">
|
||||
<div v-if="message.role === 'assistant' && (debugModeAi ? true : message.content)"
|
||||
class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]">
|
||||
<div class="flex items-center justify-center shrink-0">
|
||||
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
||||
</div>
|
||||
@@ -256,7 +273,6 @@ async function clearAllChats() {
|
||||
}" />
|
||||
|
||||
|
||||
|
||||
<div v-if="debugModeAi && !message.content">
|
||||
<div class="flex flex-col"
|
||||
v-if="message.tool_calls && message.tool_calls.length > 0">
|
||||
@@ -285,17 +301,26 @@ async function clearAllChats() {
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex items-center gap-3 justify-start w-full poppins text-[1.1rem]"
|
||||
v-if="currentChatMessageDelta">
|
||||
|
||||
<div class="flex items-center justify-center shrink-0">
|
||||
<img class="h-[3.5rem] w-auto" :src="'analyst.png'">
|
||||
</div>
|
||||
<div class="max-w-[70%] text-text/90 ai-message whitespace-pre-wrap">
|
||||
<vue-markdown :source="currentChatMessageDeltaHtml" :options="{
|
||||
|
||||
<div class="max-w-[70%] text-text/90 ai-message">
|
||||
<div v-if="currentChatMessageDeltaShowLoader" class="flex items-center gap-1">
|
||||
<i class="fas fa-loader animate-spin"></i>
|
||||
<div> Loading </div>
|
||||
</div>
|
||||
<vue-markdown :source="currentChatMessageDeltaTextVisible" :options="{
|
||||
html: true,
|
||||
breaks: true,
|
||||
}" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -356,7 +381,8 @@ async function clearAllChats() {
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="poppins font-semibold text-[1.1rem]"> History </div>
|
||||
<LyxUiButton v-if="chatsList && chatsList.length > 0" @click="clearAllChats()" type="secondary" class="text-center text-[.8rem]">
|
||||
<LyxUiButton v-if="chatsList && chatsList.length > 0" @click="clearAllChats()" type="secondary"
|
||||
class="text-center text-[.8rem]">
|
||||
Clear all
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ const getSessionsCountsTool: AIPlugin_TTool<'getSessionsCount'> = {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'getSessionsCount',
|
||||
description: 'Gets the number of sessions received on a date range',
|
||||
description: 'Gets the number of sessions (unique visitors) received on a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -83,4 +83,4 @@ export class AiSessions extends AIPlugin<['getSessionsCount', 'getSessionsTimeli
|
||||
}
|
||||
}
|
||||
|
||||
export const ASessionsInstance = new AiSessions();
|
||||
export const AiSessionsInstance = new AiSessions();
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import OpenAI from "openai";
|
||||
import { AiChatModel } from '@schema/ai/AiChatSchema';
|
||||
import { ProjectCountModel } from '@schema/project/ProjectsCounts';
|
||||
import { ProjectLimitModel } from '@schema/project/ProjectsLimits';
|
||||
|
||||
import { AiEventsInstance } from '../ai/functions/AI_Events';
|
||||
import { AiVisitsInstance } from '../ai/functions/AI_Visits';
|
||||
import { AiSessionsInstance } from '../ai/functions/AI_Sessions';
|
||||
import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart';
|
||||
|
||||
const { AI_KEY, AI_ORG, AI_PROJECT } = useRuntimeConfig();
|
||||
@@ -18,13 +18,15 @@ const openai = new OpenAI({ apiKey: AI_KEY, organization: AI_ORG, project: AI_PR
|
||||
const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
|
||||
...AiVisitsInstance.getTools(),
|
||||
...AiEventsInstance.getTools(),
|
||||
...AiComposableChartInstance.getTools()
|
||||
...AiSessionsInstance.getTools(),
|
||||
...AiComposableChartInstance.getTools(),
|
||||
]
|
||||
|
||||
|
||||
const functions: any = {
|
||||
...AiVisitsInstance.getHandlers(),
|
||||
...AiEventsInstance.getHandlers(),
|
||||
...AiSessionsInstance.getHandlers(),
|
||||
...AiComposableChartInstance.getHandlers()
|
||||
}
|
||||
|
||||
@@ -188,7 +190,7 @@ export async function sendMessageOnChat(text: string, pid: string, time_offset:
|
||||
} else {
|
||||
const roleMessage: OpenAI.Chat.Completions.ChatCompletionMessageParam = {
|
||||
role: 'system',
|
||||
content: "Today ISO date: " + new Date().toISOString()
|
||||
content: "You are an AI Data Analyst and Growth Hacker specialized in helping users analyze data collected within Litlyx and providing strategies to grow their website, app, or business. Your scope is strictly limited to data creation, visualization, and growth-related advice. If a user asks something outside this domain, politely inform them that you are not designed to answer such questions. Today ISO date is " + new Date().toISOString() + "take this in count when the user ask relative dates"
|
||||
}
|
||||
messages.push(roleMessage);
|
||||
await addMessageToChat(roleMessage, chat_id);
|
||||
|
||||
Reference in New Issue
Block a user