feat: ditch triplit, move to postgresql + drizzle orm
This commit is contained in:
@@ -2,16 +2,14 @@
|
||||
import type { BaseMessage } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const route = useRoute();
|
||||
const inputValue = ref<BaseMessage>({ content: '', fileIds: [] });
|
||||
const pendingMessage = ref<Message | null>(null);
|
||||
|
||||
const { open: sidebarOpen, openSidebar } = useSidebar();
|
||||
const { createTopic, sendMessage, autoRename } = useChat(route.params.id as string);
|
||||
const { getAgent } = useAgents();
|
||||
const { providers } = useModels();
|
||||
const { autoRename: autoRenameTopic } = useTopic();
|
||||
const { getAgent, createTopic } = await useAgents();
|
||||
const { providers } = await useModels();
|
||||
|
||||
const agent = getAgent(route.params.id as string);
|
||||
|
||||
@@ -31,10 +29,13 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("handleSubmit", message);
|
||||
if (!message.content) return;
|
||||
|
||||
pendingMessage.value = {
|
||||
id: '',
|
||||
userId: user.value!.id,
|
||||
topicId: null,
|
||||
topicId: '',
|
||||
content: message.content,
|
||||
role: 'user',
|
||||
parts: [],
|
||||
@@ -46,37 +47,33 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
attachments: [],
|
||||
deleted: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
const topic = await createTopic();
|
||||
const topic = await createTopic(agent.value!.id);
|
||||
if (!topic) throw new Error('Failed to create topic');
|
||||
|
||||
autoRename(topic.id, message.content);
|
||||
const { sendMessage, startGeneration } = await useChat(topic.id, false);
|
||||
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
|
||||
return sendMessage(message, topic, [], agent.value!, model.provider, model).then(async res => {
|
||||
sendMessage(message).then(async res => {
|
||||
if (res.ok === false) {
|
||||
console.error('Failed to send message:', res.error);
|
||||
|
||||
pendingMessage.value = null;
|
||||
|
||||
await navigateTo(`/agent/${route.params.id}`);
|
||||
await triplit.delete('topics', topic.id);
|
||||
|
||||
// const chatInput = document.getElementById('chat') as HTMLInputElement;
|
||||
// if (chatInput) {
|
||||
// chatInput.value = message;
|
||||
// chatInput.dispatchEvent(new Event('input'));
|
||||
// nextTick(() => {
|
||||
// chatInput.focus();
|
||||
// });
|
||||
// }
|
||||
nextTick(() => {
|
||||
inputValue.value = message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await navigateTo(`/agent/${route.params.id}/topic/${topic.id}`);
|
||||
|
||||
autoRenameTopic(topic.id);
|
||||
|
||||
return startGeneration(model);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -86,10 +83,10 @@ const handleSubmit = async (message: BaseMessage, model: ModelWithProvider | nul
|
||||
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
|
||||
<div class="flex items-center gap-2 max-w-full">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent hover:bg-[var(--color-hover)] transition-colors">
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
<span class="i-mynaui-panel-left-open text-5"></span>
|
||||
</button>
|
||||
<h4 class="text-lg text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
<h4 class="text-lg truncate">
|
||||
{{ agent?.name }}
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
const { getAgent } = useAgents();
|
||||
const triplit = useTriplitClient();
|
||||
const { getAgent, patchAgentLocally } = await useAgents();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { open: sidebarOpen, openSidebar } = useSidebar();
|
||||
|
||||
const agent = getAgent(route.params.id as string);
|
||||
let serverAgent = agent.value;
|
||||
|
||||
const name = ref<string | null>(agent.value?.name ?? null);
|
||||
const systemPrompt = ref<string | null>(agent.value?.systemPrompt ?? null);
|
||||
|
||||
if (agent.value === undefined) navigateTo('/');
|
||||
|
||||
const handleInput = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.value.trimStart().length === 0) {
|
||||
return;
|
||||
let debounceTimeout: NodeJS.Timeout | null = null;
|
||||
const debouncedUpdate = async (updates: Partial<Agent>) => {
|
||||
if (debounceTimeout !== null) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
|
||||
await triplit.update('agents', agent.value!.id, { name: target.value });
|
||||
patchAgentLocally(route.params.id as string, updates);
|
||||
|
||||
debounceTimeout = setTimeout(async () => {
|
||||
debounceTimeout = null;
|
||||
try {
|
||||
await $fetch(`/api/agent/${route.params.id}`, {
|
||||
method: 'PATCH',
|
||||
body: updates,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update agent:', error);
|
||||
|
||||
if (serverAgent) {
|
||||
patchAgentLocally(route.params.id as string, serverAgent);
|
||||
name.value = serverAgent.name;
|
||||
systemPrompt.value = serverAgent.systemPrompt;
|
||||
}
|
||||
}
|
||||
}, 700);
|
||||
};
|
||||
|
||||
const handleNameInput = async (e: Event) => {
|
||||
const name = (e.target as HTMLInputElement).value;
|
||||
if (!name.trim()) return;
|
||||
|
||||
debouncedUpdate({ name });
|
||||
};
|
||||
|
||||
const changeSystemPrompt = async (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
let value: string | undefined = target.value;
|
||||
|
||||
if (value.trimStart().length === 0) {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
await triplit.update('agents', agent.value!.id, {
|
||||
systemPrompt: target.value,
|
||||
});
|
||||
let systemPrompt = (e.target as HTMLTextAreaElement).value as string | null;
|
||||
if (!systemPrompt!.trim()) systemPrompt = null;
|
||||
debouncedUpdate({ systemPrompt });
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -37,7 +58,7 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
<div class="h-14 flex items-center justify-between px-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent hover:bg-[var(--color-hover)] transition-colors">
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
<span class="i-mynaui-panel-left-open text-5"></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -49,15 +70,15 @@ const changeSystemPrompt = async (e: Event) => {
|
||||
<img v-if="agent?.imageUrl" :src="agent.imageUrl" class="w-16 h-16 rounded-full object-cover" />
|
||||
<span class="text-16 i-mynaui-check-hexagon"></span>
|
||||
</div>
|
||||
<input @input="handleInput" placeholder="Agent Name..."
|
||||
<input v-model="name" @input="handleNameInput" placeholder="Agent Name..."
|
||||
class="placeholder:text-[var(--text-tertiary)] w-full bg-transparent rounded-none border-b-4 border-b-[var(--color-border)] text-12 p-0"
|
||||
type="text" :value="agent?.name" />
|
||||
type="text" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full h-full mb-14">
|
||||
<label class="text-sm text-[var(--text-secondary)]">System Message</label>
|
||||
<textarea placeholder="You are a helpful assistant."
|
||||
<textarea v-model="systemPrompt" placeholder="You are a helpful assistant."
|
||||
class="p-4 w-full h-full resize-none bg-transparent rounded-lg border border-[var(--color-border)]"
|
||||
:value="agent?.systemPrompt" @input="changeSystemPrompt"></textarea>
|
||||
@input="changeSystemPrompt"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,131 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type schema from '#triplit/schema';
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type { BaseMessage } from '~/composables/useChat';
|
||||
import { type BaseMessage, type Message, ChatErrorType } from '~/composables/useChat';
|
||||
import type { ModelWithProvider } from '~/composables/useModels';
|
||||
import { buildFocusedMessageTree } from '~~/utils/message';
|
||||
|
||||
const rootStart = Date.now();
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const chatPaneWrapper = ref<HTMLElement | null>(null);
|
||||
const inputValue = ref<BaseMessage>({ content: '', fileIds: [] });
|
||||
const route = useRoute();
|
||||
const { sendMessage, regenerateMessage } = useChat(route.params.id as string);
|
||||
const { getAgent } = useAgents();
|
||||
const { getAgent } = await useAgents();
|
||||
const { open: sidebarOpen, openSidebar } = useSidebar();
|
||||
const { providers, allModels } = useModels();
|
||||
const { providers, allModels } = await useModels();
|
||||
const { addShortcut } = useKeyboardShortcuts();
|
||||
|
||||
const agent = getAgent(route.params.id as string);
|
||||
|
||||
const topicQuery = computed(() =>
|
||||
triplit
|
||||
.query('topics')
|
||||
.Where(['id', '=', route.params.topicId])
|
||||
.Limit(1)
|
||||
);
|
||||
|
||||
const messagesQuery = computed(() =>
|
||||
triplit
|
||||
.query('messages')
|
||||
.Where(['topicId', '=', route.params.topicId])
|
||||
.Order('createdAt', 'ASC')
|
||||
);
|
||||
|
||||
const attachmentsQuery = computed(() =>
|
||||
triplit
|
||||
.query('attachments')
|
||||
.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: rawAttachments, unsubscribe: unsubscribeAttachments },
|
||||
{ results: rawParts, unsubscribe: unsubscribeParts },
|
||||
{ results: rawGenerations, unsubscribe: unsubscribeGenerations }
|
||||
] = await Promise.all([
|
||||
useQuery('topic', triplit, topicQuery),
|
||||
useQuery('messages', triplit, messagesQuery),
|
||||
useQuery('attachments', triplit, attachmentsQuery),
|
||||
useQuery('parts', triplit, partsQuery),
|
||||
useQuery('generations', triplit, generationsQuery),
|
||||
]);
|
||||
|
||||
const topic = computed(() => {
|
||||
if (!rawMessages.value || !rawTopic.value?.[0]) return null;
|
||||
|
||||
const messagesMap = new Map();
|
||||
const partsByMessage = new Map<string, Entity<typeof schema, 'message_parts'>[]>();
|
||||
const attachmentsByMessage = new Map<string, Entity<typeof schema, 'attachments'>[]>();
|
||||
|
||||
// Group parts by message ID once
|
||||
if (rawParts.value) {
|
||||
for (const part of rawParts.value) {
|
||||
if (!partsByMessage.has(part.messageId)) {
|
||||
partsByMessage.set(part.messageId, []);
|
||||
}
|
||||
if (part.content !== '' || part.toolCall !== null) {
|
||||
partsByMessage.get(part.messageId)!.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rawAttachments.value) {
|
||||
for (const attachment of rawAttachments.value) {
|
||||
if (!attachmentsByMessage.has(attachment.messageId)) {
|
||||
attachmentsByMessage.set(attachment.messageId, []);
|
||||
}
|
||||
attachmentsByMessage.get(attachment.messageId)!.push(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
const generationsMap = new Map(
|
||||
rawGenerations.value?.map(g => [g.id, g]) ?? []
|
||||
);
|
||||
|
||||
// Single pass to build messages
|
||||
for (const msg of rawMessages.value) {
|
||||
messagesMap.set(msg.id, {
|
||||
...msg,
|
||||
attachments: attachmentsByMessage.get(msg.id) ?? [],
|
||||
parts: partsByMessage.get(msg.id) ?? [],
|
||||
children: [],
|
||||
generation: generationsMap.get(msg.generationId!) ?? null
|
||||
});
|
||||
}
|
||||
|
||||
// Build tree
|
||||
const rootMessages: Message[] = [];
|
||||
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 || []
|
||||
};
|
||||
});
|
||||
const topicId = computed(() => route.params.topicId as string);
|
||||
const { topic, sendMessage, startGeneration, regenerateMessage, patchMessageLocally } = await useChat(topicId);
|
||||
|
||||
watch(() => topic.value?.name, (newTopicName) => {
|
||||
if (newTopicName !== undefined) {
|
||||
@@ -139,7 +30,14 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await sendMessage(message, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
inputValue.value = { content: '', fileIds: [] };
|
||||
|
||||
const res = await sendMessage(message, async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
scrollToBottom('instant')
|
||||
});
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error('Failed to send message:', res.error);
|
||||
const chatInput = document.getElementById('chat') as HTMLInputElement | null;
|
||||
@@ -153,33 +51,16 @@ const submitMessage = async (message: BaseMessage, model: ModelWithProvider | nu
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToBottom('instant');
|
||||
startGeneration(model);
|
||||
};
|
||||
|
||||
const focusedMessageTree = computed(() => {
|
||||
const tree: Readonly<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;
|
||||
let messageId: string;
|
||||
if (
|
||||
(message.focusedIndex !== undefined && message.focusedIndex !== null)
|
||||
&& message.focusedIndex > 0
|
||||
@@ -191,90 +72,41 @@ const handleRegenerate = async (message: Message) => {
|
||||
}
|
||||
|
||||
const model = allModels.value.find(m => m.id === agent.value!.defaultModelId);
|
||||
if (!model) {
|
||||
console.error('Model not found');
|
||||
if (!model || !model.provider) {
|
||||
console.error('Model not found or provider not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await regenerateMessage(messageId, topic.value!, focusedMessageTree.value, agent.value!, model.provider, model);
|
||||
const res = await regenerateMessage(messageId, buildFocusedMessageTree(topic.value!.messages), 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
|
||||
await $fetch(`/api/messages/${rootMessage.id}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
const flatMessages = computed(() => {
|
||||
const messages: Message[] = [];
|
||||
|
||||
for (const message of topic.value?.messages ?? []) {
|
||||
messages.push(message);
|
||||
if (message.children.length > 0) {
|
||||
messages.push(...message.children as Message[]);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
});
|
||||
|
||||
const activeGeneration = computed(() => {
|
||||
if (topic.value === null) return null;
|
||||
return topic.value?.generations?.find((generation) => generation.status === 'pending') ?? null;
|
||||
const generations = flatMessages.value.flatMap(message => message.generation);
|
||||
return generations?.find((generation) => generation?.status === 'pending') ?? null;
|
||||
});
|
||||
|
||||
const { scrollToBottom } = useAutoScroll(chatPaneWrapper);
|
||||
@@ -318,9 +150,8 @@ addShortcut(['alt', '['], async (event) => {
|
||||
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
await triplit.update('messages', lastMessage.id, {
|
||||
focusedIndex: Math.max(0, lastMessage.focusedIndex! - 1)
|
||||
});
|
||||
const newIndex = Math.max(0, (lastMessage.focusedIndex || 0) - 1);
|
||||
patchMessageLocally(lastMessage.id, { focusedIndex: newIndex });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -329,10 +160,10 @@ addShortcut(['alt', ']'], async (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
const messageCount = lastMessage ? (lastMessage.deleted ? lastMessage.children.length : lastMessage.children.length + 1) : 0;
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
await triplit.update('messages', lastMessage.id, {
|
||||
focusedIndex: Math.min(lastMessage.children.length, lastMessage.focusedIndex! + 1)
|
||||
});
|
||||
const newIndex = Math.min(messageCount - 1, (lastMessage.focusedIndex || 0) + 1);
|
||||
patchMessageLocally(lastMessage.id, { focusedIndex: newIndex });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -342,21 +173,20 @@ addShortcut(['ctrl', 'alt', 'arrowleft'], async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
await triplit.update('messages', lastMessage.id, {
|
||||
focusedIndex: Math.max(0, lastMessage.focusedIndex! - 1)
|
||||
});
|
||||
const newIndex = Math.max(0, (lastMessage.focusedIndex || 0) - 1);
|
||||
patchMessageLocally(lastMessage.id, { focusedIndex: newIndex });
|
||||
}
|
||||
})
|
||||
|
||||
addShortcut(['ctrl', 'alt', 'arrowright'], async (event) => {
|
||||
const lastMessage = topic.value?.messages?.at(-1);
|
||||
const messageCount = lastMessage ? (lastMessage.deleted ? lastMessage.children.length : lastMessage.children.length + 1) : 0;
|
||||
if (lastMessage && lastMessage.children.length > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
await triplit.update('messages', lastMessage.id, {
|
||||
focusedIndex: Math.min(lastMessage.children.length, lastMessage.focusedIndex! + 1)
|
||||
});
|
||||
const newIndex = Math.min(messageCount - 1, (lastMessage.focusedIndex || 0) + 1);
|
||||
patchMessageLocally(lastMessage.id, { focusedIndex: newIndex });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -365,20 +195,13 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const handleCancel = async () => {
|
||||
await $fetch(`/api/chat/cancel/${activeGeneration.value?.id}`, {
|
||||
// TODO: cancel generation
|
||||
await $fetch(`/api/topic/${topicId.value}/chat/cancel/${activeGeneration.value?.id}`, {
|
||||
method: 'POST',
|
||||
});
|
||||
};
|
||||
|
||||
console.log("full page render took", Date.now() - rootStart);
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribeTopic?.();
|
||||
unsubscribeMessages?.();
|
||||
unsubscribeAttachments?.();
|
||||
unsubscribeParts?.();
|
||||
unsubscribeGenerations?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -386,10 +209,10 @@ onUnmounted(() => {
|
||||
<div class="h-14 flex items-center justify-between px-4 border-b border-[var(--color-border)]">
|
||||
<div class="flex items-center gap-2 max-w-full">
|
||||
<button v-if="!sidebarOpen" @click="openSidebar"
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent hover:bg-[var(--color-hover)] transition-colors">
|
||||
class="flex p-2 rounded-lg text-[var(--text-primary)] bg-transparent @hover:bg-[var(--color-hover)] transition-colors">
|
||||
<span class="i-mynaui-panel-left-open text-5"></span>
|
||||
</button>
|
||||
<h4 class="text-lg text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
<h4 class="text-lg truncate">
|
||||
{{ topic?.name }}
|
||||
</h4>
|
||||
</div>
|
||||
@@ -403,7 +226,8 @@ onUnmounted(() => {
|
||||
<Suspense>
|
||||
<template v-if="Array.isArray(topic?.messages) && topic.messages.length > 0">
|
||||
<Message v-for="message in topic.messages" :key="message.id" :message="message"
|
||||
v-memo="[message.id, message.parts?.length, message.children, message.focusedIndex, message.content]"
|
||||
@edit="(value) => patchMessageLocally(message.id, { content: value })"
|
||||
@patch="(updates) => patchMessageLocally(message.id, updates)"
|
||||
@delete="handleDelete(message)" @regenerate="handleRegenerate(message)" />
|
||||
</template>
|
||||
</Suspense>
|
||||
|
||||
Reference in New Issue
Block a user