112 lines
3.8 KiB
Vue
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> |