Files
veridian/app/pages/agent/[id]/topic/[topicId].vue
T

112 lines
3.8 KiB
Vue

<script setup lang="ts">
import type { Message } from '~/composables/useChat';
import type { ModelWithProvider } from '~/composables/useModels';
const triplit = useTriplitClient();
const chatPane = ref<HTMLElement | null>(null);
const route = useRoute();
const { sendMessage } = useChat(route.params.id as string);
const { getAgent } = await useAgents();
const { providers, unsubscribe: unsubscribeModels } = await useModels();
const agent = computed(() => {
if (route.params.id === null || typeof route.params.id !== 'string') {
throw new Error('Invalid agent ID');
}
return getAgent(route.params.id)!;
});
const topicQuery = computed(() =>
triplit
.query('topics')
.Where(['id', '=', route.params.topicId])
.Include('generations')
.Include('messages', (rel) =>
rel('messages')
.Include('generation')
.Include('parts', (rel) => rel('parts').Include('toolCall')),
)
.Limit(1)
);
const { results, unsubscribe: unsubscribeTopic } = await useQuery('topic', triplit, topicQuery);
const topic = computed(() => {
if (results.value?.length === 0) return null;
// copy messages to a mutable object and sort by createdAt
const messages = results!.value![0]!.messages.map((message) => ({
...message,
parts: message.parts.map((part) => ({
...part,
toolCall: part.toolCall ? { ...part.toolCall } : null,
})),
}));
messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
// for each message, sort parts by createdAt
messages.forEach((message) => {
message.parts = message.parts
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
.filter((part) => part.content !== '' || part.toolCall !== null);
});
return { ...results.value![0]!, messages };
});
const activeGeneration = computed(() => {
if (topic.value === null) return null;
return topic.value?.generations?.find((generation) => generation.status === 'pending') ?? null;
});
const { scrollToBottom } = useAutoScroll(chatPane);
onMounted(() => {
scrollToBottom('instant');
});
const handleCancel = async () => {
await $fetch(`/api/chat/cancel/${activeGeneration.value!.id}`, {
method: 'POST',
});
};
const handleSubmit = async (message: string, model: ModelWithProvider | null) => {
if (!model) {
console.error('No model selected');
return;
}
await sendMessage(message, topic.value!, topic.value!.messages as unknown as Message[], agent.value!, model.provider, model);
scrollToBottom('instant');
};
onUnmounted(() => {
unsubscribeTopic?.();
unsubscribeModels?.();
});
</script>
<template>
<div class="h-full w-full">
<!-- chat pane -->
<div class="flex flex-col w-full px-4 overflow-y-auto h-full"
style="scrollbar-width: thin; scrollbar-color: #888 transparent;" ref="chatPane">
<div class="flex-grow w-full flex justify-center">
<div class="max-w-4xl w-full flex flex-col gap-2 pb-9"
v-if="Array.isArray(topic?.messages) && topic.messages.length > 0">
<Message v-for="message in topic.messages" :key="message.id" :message="message" />
</div>
</div>
<div class="sticky max-h-full z-10 bottom-0 w-full flex justify-center">
<div class="pb-4 w-full max-w-4xl bg-[var(--color-neutral)] rounded-t-2xl">
<ChatInput class="[view-transition-name:chat-prompt] duration-150 ease-in-out"
:loading="activeGeneration !== null" :agent="agent" :providers="providers"
@submit="handleSubmit" @cancel="handleCancel"></ChatInput>
</div>
</div>
</div>
</div>
</template>