Files
veridian/app/components/Message/index.vue
T

227 lines
7.8 KiB
Vue

<script setup lang="ts">
import type { Message } from '~/composables/useChat';
const { openDialog } = useDialog();
const { message } = defineProps<{
message: Message
}>();
const emit = defineEmits<{
regenerate: [];
delete: [];
edit: [value: string];
patch: [updates: Partial<Message>];
fork: [];
}>();
let reqAbortController: AbortController | null = null;
watch(() => message.activeChildId, async (newValue) => {
if (reqAbortController) {
reqAbortController.abort();
reqAbortController = null;
}
reqAbortController = new AbortController();
await $fetch(`/api/messages/${message.id}`, {
method: 'PATCH',
body: {
activeChildId: newValue
},
signal: reqAbortController.signal,
});
});
watch(() => message.children.length, (newCount, oldCount) => {
if (newCount === oldCount) return;
if (newCount === 0) {
emit('patch', { activeChildId: null });
return;
}
if (oldCount > newCount) {
const activeChild = message.children.find(c => c?.id === message.activeChildId);
if (!activeChild) {
emit('patch', { activeChildId: message.children[newCount - 1]?.id ?? null });
}
return;
}
const newChild = message.children[newCount - 1];
if (newChild) {
emit('patch', { activeChildId: newChild.id });
}
});
const activeMessage = computed(() => {
if (message.activeChildId && message.children.length > 0) {
const child = message.children.find(c => c?.id === message.activeChildId);
if (child) return child;
}
if (message.deleted === true && message.children.length > 0) {
return message.children[0];
}
return message;
});
const copied = ref(false);
const copyMessage = async () => {
if (activeMessage.value!.role === 'user') {
await navigator.clipboard.writeText(activeMessage.value!.content!);
} else {
let text = [];
for (const part of activeMessage.value!.parts || []) {
if (part.type === 'text') {
text.push(part.content);
}
}
await navigator.clipboard.writeText(text.join('\n\n'));
}
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 2000);
}
const regenerateMessage = async () => {
emit('regenerate');
}
const handleEdit = async () => {
const originalContent = message.content!;
openDialog<string>(DialogType.Textbox, async (res) => {
if (res.ok) {
if (!res.data) return;
const req = await $fetch(`/api/messages/${message.id}`, {
method: 'PATCH',
body: {
content: res.data
},
onRequest() {
message.content = res.data;
emit('edit', res.data);
},
onResponseError() {
message.content = originalContent;
emit('edit', originalContent);
},
});
if (!req.ok) {
return;
}
await regenerateMessage();
}
}, {
title: 'Edit Message',
initialValue: message.content!
});
};
const forkMessage = () => {
emit('fork');
};
const deleteMessage = () => {
emit('delete');
};
const messageCount = computed(() => {
if (message.deleted === true) {
return message.children.length;
}
return message.children.length + 1;
});
const currentChildIndex = computed(() => {
if (!message.activeChildId) return 0;
const idx = message.children.findIndex(c => c?.id === message.activeChildId);
return idx >= 0 ? idx + 1 : 0;
});
const navigateChild = (direction: -1 | 1) => {
const allItems = message.deleted === true
? message.children
: [null, ...message.children];
const currentIdx = allItems.findIndex(item =>
!item ? !message.activeChildId : item.id === message.activeChildId
);
const newIdx = Math.max(0, Math.min(allItems.length - 1, currentIdx + direction));
const newItem = allItems[newIdx];
emit('patch', { activeChildId: newItem?.id ?? null });
};
</script>
<template>
<div v-if="activeMessage" class="max-w-full mb-4 group flex flex-col">
<div :class="[message.role === 'user' ? 'pl-9 flex justify-end' : '']">
<MessageUser v-if="message.role === 'user'" :message="activeMessage!" />
<MessageAgent v-else :message="activeMessage" />
</div>
<div class="flex justify-between items-center">
<div>
<div v-if="messageCount > 1" class="flex gap-1">
<Tooltip :inert="currentChildIndex === 0" :hotkey="['alt', '[']">
<button @click="navigateChild(-1)"
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="i-mynaui-chevron-left w-4 h-4 text-[var(--text-secondary)]"
:class="currentChildIndex === 0 ? 'opacity-0' : ''"></span>
</button>
</Tooltip>
<span class="text-xs text-[var(--text-secondary)]">
{{ currentChildIndex + 1 }} / {{ messageCount }}
</span>
<Tooltip :inert="currentChildIndex + 1 === messageCount" :hotkey="['alt', ']']">
<button @click="navigateChild(1)"
class="@hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<span class="i-mynaui-chevron-right w-4 h-4 text-[var(--text-secondary)]"
:class="currentChildIndex + 1 === messageCount ? 'opacity-0' : ''"></span>
</button>
</Tooltip>
</div>
</div>
<div v-show="activeMessage.generation?.status !== 'pending'"
class="self-end mt-1 w-fit bg-[var(--bg-container)] text-[var(--text-secondary)] gap-px flex items-center rounded-md overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<Tooltip :hotkey="['ctrl', 'shift', 'enter']">
<button @click="regenerateMessage"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-refresh text-4.5"></span>
</button>
</Tooltip>
<button v-if="message.role === 'user'" @click="handleEdit"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-pencil text-4.5"></span>
</button>
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span :class="copied ? 'i-mynaui-check text-emerald-500' : 'i-mynaui-copy text-4.5'"></span>
</button>
<button @click="forkMessage"
class="flex justify-center items-center w-7 h-6 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-git-branch text-4.5"></span>
</button>
<Tooltip :hotkey="['ctrl', 'shift', 'backspace']">
<button @click="deleteMessage"
class="flex justify-center items-center w-7 h-6 text-red-500 @hover:bg-[var(--color-hover)]">
<span class="i-mynaui-trash text-5"></span>
</button>
</Tooltip>
</div>
</div>
</div>
</template>