236 lines
8.2 KiB
Vue
236 lines
8.2 KiB
Vue
<script setup lang="ts">
|
|
import type { Message } from '~~/types';
|
|
|
|
const route = useRoute();
|
|
const appState = useAppState();
|
|
const { activeTopic, topicsForActiveAgent } = await useTopics();
|
|
const { activeAgent } = await useAgents();
|
|
|
|
const loading = ref(false);
|
|
const messages = useState<Message[]>('messages', () => []);
|
|
const generatingMessage = ref('');
|
|
|
|
// Fetch messages for this topic
|
|
if (activeTopic.value) {
|
|
const topicData = await useFetch(`/api/topics/${activeTopic.value.id}`);
|
|
if (topicData.error.value) {
|
|
console.error('Failed to load topic:', topicData.error.value);
|
|
} else if (topicData.data.value?.messages) {
|
|
messages.value = topicData.data.value.messages;
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (message: string) => {
|
|
if (!activeTopic.value) {
|
|
console.error('No active topic');
|
|
return;
|
|
}
|
|
|
|
loading.value = true;
|
|
|
|
try {
|
|
// Add user message to local state
|
|
const userMessage: Message = {
|
|
id: `temp_${Date.now()}`,
|
|
topicId: activeTopic.value.id,
|
|
userId: '',
|
|
content: message,
|
|
isUser: true,
|
|
regeneratedFromId: null,
|
|
isRegenerated: false,
|
|
editedAt: null,
|
|
createdAt: new Date() as any
|
|
};
|
|
messages.value.push(userMessage);
|
|
|
|
// Create generation
|
|
const generation = await $fetch(`/api/chat/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
topicId: activeTopic.value.id,
|
|
messages: messages.value
|
|
.filter(m => m.content)
|
|
.map(m => ({
|
|
type: m.isUser ? 'user' : 'agent',
|
|
message: m.content
|
|
}))
|
|
})
|
|
});
|
|
|
|
appState.startGeneration(generation.generationId);
|
|
|
|
// Stream the response
|
|
const response = await $fetch<ReadableStream>(`/api/chat/stream/${generation.generationId}`, {
|
|
method: 'get',
|
|
responseType: 'stream',
|
|
});
|
|
|
|
const reader = response.pipeThrough(new TextDecoderStream()).getReader();
|
|
generatingMessage.value = '';
|
|
let hasError = false;
|
|
|
|
while (!hasError) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
try {
|
|
const lines = value.split('\n').filter(line => line.trim());
|
|
for (const line of lines) {
|
|
const event = JSON.parse(line);
|
|
|
|
if (event.type === 'start') {
|
|
generatingMessage.value = '';
|
|
} else if (event.type === 'token') {
|
|
generatingMessage.value += event.data;
|
|
} else if (event.type === 'complete') {
|
|
// Add the completed message to the list
|
|
console.log(event, event.data);
|
|
if (event.data) {
|
|
messages.value.concat(event.data);
|
|
generatingMessage.value = '';
|
|
}
|
|
} else if (event.type === 'error') {
|
|
console.error('Generation error:', event.data);
|
|
hasError = true;
|
|
}
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Failed to parse event:', parseError);
|
|
}
|
|
}
|
|
|
|
appState.endGeneration();
|
|
} catch (error) {
|
|
console.error('Failed to submit message:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handleRegenerate = async (messageId: string) => {
|
|
if (!activeTopic.value) return;
|
|
|
|
// Find the user message before this one to regenerate context
|
|
const messageIndex = messages.value.findIndex(m => m.id === messageId);
|
|
if (messageIndex === -1) return;
|
|
|
|
const previousUserMessage = messages.value[messageIndex - 1];
|
|
if (!previousUserMessage) return;
|
|
|
|
loading.value = true;
|
|
|
|
try {
|
|
// Create generation with previous user message
|
|
const generation = await $fetch(`/api/chat/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
topicId: activeTopic.value.id,
|
|
regeneratesFrom: messageId,
|
|
messages: messages.value.slice(0, messageIndex)
|
|
.map(m => ({
|
|
type: m.isUser ? 'user' : 'agent',
|
|
message: m.content
|
|
}))
|
|
})
|
|
});
|
|
|
|
appState.startGeneration(generation.generationId);
|
|
|
|
// Stream the response
|
|
const response = await $fetch<ReadableStream>(`/api/chat/stream/${generation.generationId}`, {
|
|
method: 'get',
|
|
responseType: 'stream',
|
|
});
|
|
|
|
const reader = response.pipeThrough(new TextDecoderStream()).getReader();
|
|
generatingMessage.value = '';
|
|
let hasError = false;
|
|
|
|
while (!hasError) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
try {
|
|
const lines = value.split('\n').filter(line => line.trim());
|
|
for (const line of lines) {
|
|
const event = JSON.parse(line);
|
|
|
|
if (event.type === 'start') {
|
|
generatingMessage.value = '';
|
|
} else if (event.type === 'token') {
|
|
generatingMessage.value += event.data;
|
|
} else if (event.type === 'complete') {
|
|
// Add the new regenerated message
|
|
if (event.data) {
|
|
messages.value.push(event.data);
|
|
generatingMessage.value = '';
|
|
}
|
|
} else if (event.type === 'error') {
|
|
console.error('Generation error:', event.data);
|
|
hasError = true;
|
|
}
|
|
}
|
|
} catch (parseError) {
|
|
console.error('Failed to parse event:', parseError);
|
|
}
|
|
}
|
|
|
|
appState.endGeneration();
|
|
} catch (error) {
|
|
console.error('Failed to regenerate:', error);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handleSelectRegeneration = (messageId: string) => {
|
|
const index = messages.value.findIndex(m => m.id === messageId);
|
|
if (index === -1) return;
|
|
|
|
// In a real app, you'd update the UI to show the selected version
|
|
// For now, just highlight it
|
|
console.log('Selected regeneration:', messageId);
|
|
};
|
|
|
|
const handleDelete = (messageId: string) => {
|
|
const index = messages.value.findIndex(m => m.id === messageId);
|
|
if (index === -1) return;
|
|
|
|
messages.value.splice(index, 1);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex h-full w-full pb-4 justify-center">
|
|
<div class="max-w-4xl h-full w-full flex flex-col">
|
|
<div class="flex h-full flex-col gap-6 overflow-y-auto p-4">
|
|
<Message v-for="msg in messages" :key="msg.id" :message="msg" @regenerate="handleRegenerate"
|
|
@select="handleSelectRegeneration" @delete="handleDelete" />
|
|
|
|
<div v-if="generatingMessage" class="flex gap-3">
|
|
<div
|
|
class="flex-shrink-0 w-8 h-8 rounded-lg bg-[var(--color-neutral)] border border-[var(--color-highlight)] flex items-center justify-center">
|
|
<Icon name="mynaui:check-hexagon" class="w-4 h-4 text-[var(--color-accent)]" />
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-[var(--color-neutral)]">Agent</p>
|
|
<p class="text-sm text-[var(--color-text)]">{{ generatingMessage }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<p v-if="messages.length === 0 && !generatingMessage" class="text-center text-[var(--color-subtle)]">
|
|
No messages yet. Start the conversation!
|
|
</p>
|
|
</div>
|
|
|
|
<ChatInput @submit="handleSubmit" :loading="loading" />
|
|
</div>
|
|
</div>
|
|
</template>
|