feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||
|
||||
const route = useRoute();
|
||||
const pendingMessage = ref<Message | null>(null);
|
||||
|
||||
const { createTopic, sendMessage } = useChat(route.params.id as string);
|
||||
const { getAgent } = await useAgents();
|
||||
const { createTopic, sendMessage, autoRename } = useChat(route.params.id as string);
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
||||
|
||||
const agent = computed(() => {
|
||||
@@ -15,22 +16,49 @@ const agent = computed(() => {
|
||||
return getAgent(route.params.id)!;
|
||||
});
|
||||
|
||||
if (!agent.value) navigateTo('/');
|
||||
|
||||
const handleSubmit = async (message: string, model: ModelWithProvider | null) => {
|
||||
if (!model) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const user = useAuth().user;
|
||||
if (!user) {
|
||||
console.error('No user');
|
||||
return;
|
||||
}
|
||||
|
||||
pendingMessage.value = {
|
||||
id: '',
|
||||
userId: user.value!.id,
|
||||
topicId: null,
|
||||
content: message,
|
||||
role: 'user',
|
||||
parts: [],
|
||||
generation: null,
|
||||
parentMessageId: null,
|
||||
children: [],
|
||||
generationId: null,
|
||||
focusedIndex: null,
|
||||
deleted: false,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
const topic = await createTopic();
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
sendMessage(message, topic, [], agent.value!, model.provider, model);
|
||||
autoRename(topic.id, message);
|
||||
|
||||
return navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
|
||||
return sendMessage(message, topic, [], agent.value!, model.provider, model);
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeModels?.()
|
||||
unsubscribeAgents?.();
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -41,16 +69,22 @@ onUnmounted(() => {
|
||||
<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="flex h-full max-w-4xl w-full flex-col gap-2 justify-end">
|
||||
<h1 v-if="agent" class="font-bold">{{ agent.name }}</h1>
|
||||
<p class="mb-28 text-[var(--color-muted)]">Select a topic to continue or create a new one</p>
|
||||
<div class="flex h-full max-w-4xl w-full flex-col gap-2"
|
||||
:class="pendingMessage === null ? 'justify-end' : ''">
|
||||
<div v-if="pendingMessage === null">
|
||||
<h1 v-if="agent" class="font-bold">{{ agent.name }}</h1>
|
||||
<p class="mb-28 text-[var(--color-muted)]">Select a topic to continue or create a new one</p>
|
||||
</div>
|
||||
<div class="opacity-70" v-else>
|
||||
<Message :message="pendingMessage" />
|
||||
</div>
|
||||
</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" :agent="agent"
|
||||
:providers="providers" @submit="handleSubmit"></ChatInput>
|
||||
:providers="providers.filter(p => p.enabled)" @submit="handleSubmit"></ChatInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const { getAgent } = await useAgents();
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const route = useRoute();
|
||||
@@ -35,6 +35,10 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
systemPrompt: target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeAgents?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -48,8 +52,9 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
class="placeholder:text-[var(--color-highlight)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-highlight-high)] text-12 p-0"
|
||||
type="text" :value="agent?.name" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 w-full h-full mb-14">
|
||||
<textarea placeholder="System Message..."
|
||||
<div class="flex flex-col gap-2 w-full h-full mb-14">
|
||||
<label class="text-sm text-[var(--color-text-subtle)]">System Message</label>
|
||||
<textarea placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-highlight)]"
|
||||
:value="agent?.systemPrompt" @input="changeSystemPrompt"></textarea>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import type { Message, MessageEntity } from '~/composables/useChat';
|
||||
import type { ModelWithProvider, ProviderWithModels } 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 { sendMessage, regenerateMessage } = useChat(route.params.id as string);
|
||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
||||
const { providers, unsubscribe: unsubscribeModels, allModels } = await useModels();
|
||||
|
||||
const agent = computed(() => {
|
||||
if (route.params.id === null || typeof route.params.id !== 'string') {
|
||||
@@ -22,39 +22,90 @@ 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 messagesQuery = computed(() =>
|
||||
triplit
|
||||
.query('messages')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
.Order('createdAt', 'ASC')
|
||||
);
|
||||
|
||||
const partsQuery = computed(() =>
|
||||
triplit
|
||||
.query('message_parts')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
.Order('createdAt', 'ASC')
|
||||
.Include('toolCall')
|
||||
);
|
||||
|
||||
const generationsQuery = computed(() =>
|
||||
triplit
|
||||
.query('generations')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
);
|
||||
|
||||
const [
|
||||
{ results: rawTopic, unsubscribe: unsubscribeTopic },
|
||||
{ results: rawMessages, unsubscribe: unsubscribeMessages },
|
||||
{ results: rawParts, unsubscribe: unsubscribeParts },
|
||||
{ results: rawGenerations, unsubscribe: unsubscribeGenerations }
|
||||
] = await Promise.all([
|
||||
useQuery('topic', triplit, topicQuery),
|
||||
useQuery('messages', triplit, messagesQuery),
|
||||
useQuery('parts', triplit, partsQuery),
|
||||
useQuery('generations', triplit, generationsQuery),
|
||||
]);
|
||||
|
||||
const topic = computed(() => {
|
||||
if (results.value?.length === 0) return null;
|
||||
if (!rawMessages.value || !rawTopic.value || !rawTopic.value[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());
|
||||
// Build the messages tree manually for maximum performance
|
||||
const messagesMap = new Map();
|
||||
|
||||
// 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 };
|
||||
// First pass: Create message objects with parts arrays
|
||||
for (const msg of rawMessages.value) {
|
||||
messagesMap.set(msg.id, {
|
||||
...msg,
|
||||
parts: [],
|
||||
children: [],
|
||||
generation: rawGenerations.value?.find(g => g.id === msg.generationId) ?? null
|
||||
});
|
||||
}
|
||||
|
||||
// Second pass: Attach parts to messages
|
||||
if (rawParts.value) {
|
||||
for (const part of rawParts.value) {
|
||||
const msg = messagesMap.get(part.messageId);
|
||||
if (msg) {
|
||||
// Filter empty parts here if needed, or just push
|
||||
if (part.content !== '' || part.toolCall !== null) {
|
||||
msg.parts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: Build children relationships
|
||||
const rootMessages = [];
|
||||
for (const msg of messagesMap.values()) {
|
||||
if (msg.parentMessageId && messagesMap.has(msg.parentMessageId)) {
|
||||
messagesMap.get(msg.parentMessageId).children.push(msg);
|
||||
} else {
|
||||
rootMessages.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rawTopic.value[0],
|
||||
messages: rootMessages as Message[],
|
||||
generations: rawGenerations.value || []
|
||||
};
|
||||
});
|
||||
|
||||
if (!topic.value) navigateTo(`/agent/${route.params.id}`);
|
||||
|
||||
const activeGeneration = computed(() => {
|
||||
if (topic.value === null) return null;
|
||||
return topic.value?.generations?.find((generation) => generation.status === 'pending') ?? null;
|
||||
@@ -78,12 +129,137 @@ const handleSubmit = async (message: string, model: ModelWithProvider | null) =>
|
||||
return;
|
||||
}
|
||||
|
||||
await sendMessage(message, topic.value!, topic.value!.messages as unknown as Message[], agent.value!, model.provider, model);
|
||||
const res = await sendMessage(message, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to send message:', res.error);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToBottom('instant');
|
||||
};
|
||||
|
||||
const focusedMessageTree = computed(() => {
|
||||
const tree: MessageEntity[] = [];
|
||||
for (const message of topic.value?.messages || []) {
|
||||
if (message.focusedIndex !== undefined && message.focusedIndex !== null) {
|
||||
if (message.focusedIndex === 0) {
|
||||
tree.push(message);
|
||||
continue;
|
||||
}
|
||||
|
||||
tree.push(message.children[message.focusedIndex - 1]!);
|
||||
} else {
|
||||
tree.push(message);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
})
|
||||
|
||||
const handleRegenerate = async (message: Message) => {
|
||||
if (!agent.value.defaultModelId) {
|
||||
console.error('No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
let messageId;
|
||||
if (
|
||||
(message.focusedIndex !== undefined && message.focusedIndex !== null)
|
||||
&& message.focusedIndex > 0
|
||||
&& message.children.length > 0
|
||||
) {
|
||||
messageId = message.children[message.focusedIndex - 1]!.id;
|
||||
} else {
|
||||
messageId = message.id;
|
||||
}
|
||||
|
||||
const model = allModels.value.find(m => m.id === agent.value.defaultModelId);
|
||||
if (!model) {
|
||||
console.error('Model not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await regenerateMessage(messageId, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
if (!res.ok) {
|
||||
console.error('Failed to regenerate message:', ChatErrorType[res.error]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const deeplyDeleteMessage = async (message: MessageEntity) => {
|
||||
triplit.delete('messages', message.id);
|
||||
|
||||
if (message.generationId !== null && message.generationId !== undefined) {
|
||||
triplit.delete('generations', message.generationId);
|
||||
}
|
||||
|
||||
for (const part of message.parts || []) {
|
||||
triplit.delete('message_parts', part.id);
|
||||
}
|
||||
|
||||
if (topic.value?.messages.filter(m => m.id !== message.id).length === 0) {
|
||||
triplit.delete('topics', topic.value!.id);
|
||||
return navigateTo(`/agent/${route.params.id}/`);
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (rootMessage: Message) => {
|
||||
if (rootMessage.role === 'user') {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rootMessage.deleted === true) {
|
||||
const message = rootMessage.children[rootMessage.focusedIndex!];
|
||||
if (!message) {
|
||||
console.error('Message not found');
|
||||
return;
|
||||
}
|
||||
|
||||
deeplyDeleteMessage(message);
|
||||
|
||||
if (rootMessage.children.filter(child => child!.id !== message.id).length === 0) {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// - If the message has children, check if they are all soft deleted
|
||||
// - If they are all soft deleted, delete the message
|
||||
// - If they are not all soft deleted, mark only this message as deleted
|
||||
if (
|
||||
(rootMessage.focusedIndex !== undefined && rootMessage.focusedIndex !== null)
|
||||
&& rootMessage.focusedIndex > 0
|
||||
&& rootMessage.children.length > 0
|
||||
) {
|
||||
// we are a child message
|
||||
const message = rootMessage.children[rootMessage.focusedIndex - 1]!;
|
||||
if (!message) {
|
||||
console.error('Message not found');
|
||||
return;
|
||||
}
|
||||
|
||||
deeplyDeleteMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// we have no children
|
||||
if (rootMessage.children.length === 0) {
|
||||
deeplyDeleteMessage(rootMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// we are a root message and we have at least one living child, soft delete
|
||||
await triplit.update('messages', rootMessage.id, {
|
||||
deleted: true
|
||||
});
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeTopic?.();
|
||||
unsubscribeAgents?.();
|
||||
unsubscribeMessages?.();
|
||||
unsubscribeParts?.();
|
||||
unsubscribeGenerations?.();
|
||||
unsubscribeModels?.();
|
||||
});
|
||||
</script>
|
||||
@@ -96,15 +272,17 @@ onUnmounted(() => {
|
||||
<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" />
|
||||
<Message v-for="message in topic.messages" @delete="handleDelete(message)" :key="message.id"
|
||||
@regenerate="handleRegenerate(message)" :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>
|
||||
:loading="activeGeneration !== null" :agent="agent"
|
||||
:providers="providers?.filter(p => p.enabled)" @submit="handleSubmit" @cancel="handleCancel">
|
||||
</ChatInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user