feat: ditch triplit, move to postgresql + drizzle orm
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { MessagePart } from '~/composables/useChat';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<Entity<typeof schema, 'message_parts'>>;
|
||||
part: Readonly<MessagePart>;
|
||||
}>();
|
||||
|
||||
const reasoningOpen = ref(!props.part.finished);
|
||||
@@ -30,6 +29,12 @@ const handleScroll = () => {
|
||||
|
||||
const topThreshold = 0.02 * clientHeight;
|
||||
|
||||
if (scrollHeight <= clientHeight) {
|
||||
// the container does not have enough content to scroll
|
||||
scrollState.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTop <= topThreshold) {
|
||||
scrollState.value = 'top';
|
||||
} else if (scrollTop + 100 >= scrollHeight - clientHeight) {
|
||||
@@ -53,7 +58,7 @@ const toggleReasoning = async () => {
|
||||
<template>
|
||||
<div class="text-[--text-tertiary]">
|
||||
<button @click="toggleReasoning" :class="[
|
||||
'w-full hover:bg-[var(--color-hover)] rounded-lg p-1 flex justify-between items-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
'w-full @hover:bg-[var(--color-hover)] rounded-lg p-1 flex justify-between items-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
part.finished ? '' : 'cursor-default'
|
||||
]">
|
||||
<span class="flex items-center gap-1">
|
||||
@@ -69,7 +74,7 @@ const toggleReasoning = async () => {
|
||||
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll"
|
||||
:class="['reasoning-contaizner max-h-[min(40vh,320px)] overflow-y-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]', scrollState]">
|
||||
<div class="p-2">
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content" />
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { MessagePart } from '~/composables/useChat';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<Entity<typeof schema, 'message_parts'>>;
|
||||
part: Readonly<MessagePart>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content" />
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content!" />
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { ToolCall } from '~/composables/useChat';
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<Entity<typeof schema, 'tool_calls'>>;
|
||||
toolCall: Readonly<ToolCall>;
|
||||
}>();
|
||||
|
||||
const activeTab = ref('input');
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { ToolCall } from '~/composables/useChat';
|
||||
import Debug from './Debug.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<Entity<typeof schema, 'tool_calls'>>;
|
||||
toolCall: Readonly<ToolCall>;
|
||||
}>();
|
||||
|
||||
const deubgToolCallOpen = ref(false);
|
||||
@@ -17,7 +16,7 @@ const toggleDebugToolCall = () => {
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="select-none w-full hover:bg-[var(--color-hover)] group rounded-lg p-1 flex justify-between items-center text-[--text-tertiary] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="select-none w-full @hover:bg-[var(--color-hover)] group rounded-lg p-1 flex justify-between items-center text-[--text-tertiary] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
@@ -34,7 +33,7 @@ const toggleDebugToolCall = () => {
|
||||
<div
|
||||
class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<button @click="toggleDebugToolCall"
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden hover:bg-[var(--color-hover)] flex items-center justify-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden @hover:bg-[var(--color-hover)] flex items-center justify-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="i-mynaui-search w-3 h-3 text-[var(--text-secondary)]"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ const toggleTokenDropdown = () => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div v-for="part in message.parts" :key="part.id">
|
||||
<div v-for="part in message.parts?.filter(p => p.content?.trim() !== '' || p.toolCall)" :key="part.id">
|
||||
<Reasoning v-if="part.type === 'reasoning'" :part="part" />
|
||||
<Text v-else-if="part.type === 'text'" :part="part" />
|
||||
<Tool v-else-if="part.type === 'tool-call'" :toolCall="part.toolCall!" />
|
||||
@@ -54,7 +54,7 @@ const toggleTokenDropdown = () => {
|
||||
</div>
|
||||
|
||||
<button ref="triggerRef" @click="toggleTokenDropdown"
|
||||
class="flex gap-2 hover:bg-[var(--color-hover)] px-1 rounded transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
class="flex gap-2 @hover:bg-[var(--color-hover)] px-1 rounded transition-colors duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||
<span class="i-tabler-coins"></span>
|
||||
{{ message.generation?.tokens?.output }}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { useFloating, offset, flip, shift, autoUpdate } from '@floating-ui/vue';
|
||||
import { useFloating, offset, flip, shift, autoUpdate, hide } from '@floating-ui/vue';
|
||||
|
||||
const tokenDetailsDropdownRef = ref<HTMLElement | null>(null);
|
||||
const { triggerElement, activeGeneration: generation, close, isOpen } = useTokenDropdown();
|
||||
|
||||
const { floatingStyles } = useFloating(triggerElement, tokenDetailsDropdownRef, {
|
||||
const { floatingStyles, middlewareData } = useFloating(triggerElement, tokenDetailsDropdownRef, {
|
||||
placement: 'bottom-end',
|
||||
whileElementsMounted: autoUpdate,
|
||||
middleware: [
|
||||
offset(4),
|
||||
flip(),
|
||||
shift({ padding: 10 }),
|
||||
hide(),
|
||||
],
|
||||
transform: false,
|
||||
});
|
||||
@@ -57,7 +58,12 @@ const formatNumber = (num: number | null | undefined) => {
|
||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
||||
|
||||
<div v-if="isOpen && generation && generation.tokens" v-click-outside="close" ref="tokenDetailsDropdownRef"
|
||||
:style="floatingStyles"
|
||||
:style="{
|
||||
...floatingStyles,
|
||||
visibility: middlewareData.hide?.referenceHidden
|
||||
? 'hidden'
|
||||
: 'visible',
|
||||
}"
|
||||
class="absolute z-50 flex flex-col min-w-[280px] bg-[var(--bg-surface)] rounded-md border border-[var(--color-border)] shadow-2xl text-[var(--text-secondary)] font-sans">
|
||||
|
||||
<!-- Output Details Section -->
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import * as schema from '~~/drizzle/schema';
|
||||
|
||||
defineProps<{
|
||||
message: Readonly<Entity<typeof schema, 'messages'> & { attachments: Entity<typeof schema, 'attachments'>[] }>;
|
||||
message: Readonly<typeof schema.messages.$inferSelect & { attachments: (typeof schema.attachments.$inferSelect & { file: typeof schema.files.$inferSelect })[] }>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 max-w-full bg-[var(--bg-container)] py-2 px-3 rounded-xl">
|
||||
<MarkdownRenderer :finished="true" :content="message.content!" :id="message.id" />
|
||||
<div v-if="message.attachments.length > 0" class="flex flex-col gap-2">
|
||||
<div v-if="message.attachments && message.attachments.length > 0" class="flex flex-col gap-2">
|
||||
<div v-for="attachment in message.attachments" :key="attachment.id"
|
||||
class="flex flex-wrap items-center gap-2">
|
||||
<div
|
||||
class="flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
|
||||
<AttachmentDisplay :file="attachment" />
|
||||
<AttachmentDisplay :file="attachment.file" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~/composables/useChat';
|
||||
import { assert } from '~~/utils/assert';
|
||||
|
||||
const triplit = useTriplitClient();
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const { message } = defineProps<{
|
||||
message: Message
|
||||
}>();
|
||||
|
||||
const focusedIndex = computed({
|
||||
get: () => {
|
||||
return message.focusedIndex || 0;
|
||||
},
|
||||
set: (newValue) => {
|
||||
triplit.update('messages', message.id, {
|
||||
focusedIndex: newValue
|
||||
const emit = defineEmits<{
|
||||
regenerate: [];
|
||||
delete: [];
|
||||
edit: [value: string];
|
||||
patch: [updates: Partial<Message>];
|
||||
}>();
|
||||
|
||||
let reqAbortController: AbortController | null = null;
|
||||
|
||||
watch(() => message.focusedIndex, async (newValue) => {
|
||||
if (newValue !== undefined) {
|
||||
if (reqAbortController) {
|
||||
reqAbortController.abort();
|
||||
reqAbortController = null;
|
||||
}
|
||||
|
||||
reqAbortController = new AbortController();
|
||||
|
||||
await $fetch(`/api/messages/${message.id}`, {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
focusedIndex: newValue
|
||||
},
|
||||
signal: reqAbortController.signal,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -27,44 +42,42 @@ watch(() => message.children.length, (newCount, oldCount) => {
|
||||
// e.g. if we we have 4 children, and are focused on the 2nd, if we delete it,
|
||||
// we want to keep the focus on the 2nd index. It just makes me feel better
|
||||
if (oldCount > newCount) {
|
||||
if (message.deleted === true && focusedIndex.value === newCount) {
|
||||
focusedIndex.value = Math.max(0, newCount - 1);
|
||||
if (message.deleted === true && (message.focusedIndex || 0) === newCount) {
|
||||
console.log('focusedIndex Math.max(0, newCount - 1)');
|
||||
message.focusedIndex = Math.max(0, newCount - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedIndex.value > newCount) {
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
if ((message.focusedIndex || 0) > newCount) {
|
||||
console.log('focusedIndex newCount');
|
||||
message.focusedIndex = newCount;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
focusedIndex.value = newCount - 1;
|
||||
console.log('focusedIndex newCount - 1');
|
||||
message.focusedIndex = newCount - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
console.log('focusedIndex newCount');
|
||||
message.focusedIndex = newCount;
|
||||
});
|
||||
|
||||
const activeMessage = computed(() => {
|
||||
if (message.children.length === 0 || (focusedIndex.value === 0 && !message.deleted)) {
|
||||
if (message.children.length === 0 || ((message.focusedIndex || 0) === 0 && !message.deleted)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
return message.children[Math.min(focusedIndex.value, message.children.length - 1)];
|
||||
return message.children[Math.min((message.focusedIndex || 0), message.children.length - 1)];
|
||||
}
|
||||
|
||||
return message.children[focusedIndex.value - 1];
|
||||
return message.children[message.focusedIndex! - 1];
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
regenerate: []
|
||||
delete: []
|
||||
}>();
|
||||
|
||||
const copied = ref(false);
|
||||
const copyMessage = async () => {
|
||||
if (activeMessage.value!.role === 'user') {
|
||||
@@ -91,33 +104,44 @@ const regenerateMessage = async () => {
|
||||
}
|
||||
|
||||
const handleEdit = async () => {
|
||||
const originalContent = message.content!;
|
||||
|
||||
openDialog<string>(DialogType.Textbox, async (res) => {
|
||||
if (res.ok) {
|
||||
if (!res.data) return;
|
||||
|
||||
await triplit.update('messages', message.id, {
|
||||
content: res.data
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
assert('flush' in triplit);
|
||||
await triplit.flush();
|
||||
if (!req.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
await regenerateMessage();
|
||||
}
|
||||
}, {
|
||||
title: 'Edit Message',
|
||||
initialValue: message.content
|
||||
initialValue: message.content!
|
||||
});
|
||||
};
|
||||
|
||||
const deleteMessage = () => {
|
||||
if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
||||
focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
||||
}
|
||||
// if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
||||
// focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
||||
// }
|
||||
|
||||
nextTick(() => {
|
||||
emit('delete');
|
||||
});
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
const messageCount = computed(() => {
|
||||
@@ -138,21 +162,21 @@ const messageCount = computed(() => {
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<div v-if="messageCount > 1" class="flex gap-1">
|
||||
<Tooltip :inert="focusedIndex === 0" :hotkey="['alt', '[']">
|
||||
<button @click="focusedIndex = focusedIndex! - 1"
|
||||
class="hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Tooltip :inert="message.focusedIndex === 0" :hotkey="['alt', '[']">
|
||||
<button @click="emit('patch', { focusedIndex: message.focusedIndex! - 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="focusedIndex === 0 ? 'opacity-0' : ''"></span>
|
||||
:class="message.focusedIndex === 0 ? 'opacity-0' : ''"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span class="text-xs text-[var(--text-secondary)]">
|
||||
{{ focusedIndex + 1 }} / {{ messageCount }}
|
||||
{{ (message.focusedIndex || 0) + 1 }} / {{ messageCount }}
|
||||
</span>
|
||||
<Tooltip :inert="focusedIndex + 1 === messageCount" :hotkey="['alt', ']']">
|
||||
<button @click="focusedIndex = focusedIndex + 1"
|
||||
class="hover:bg-[var(--color-hover)] rounded transition duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Tooltip :inert="(message.focusedIndex || 0) + 1 === messageCount" :hotkey="['alt', ']']">
|
||||
<button @click="emit('patch', { focusedIndex: (message.focusedIndex || 0) + 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="focusedIndex + 1 === messageCount ? 'opacity-0' : ''"></span>
|
||||
:class="(message.focusedIndex || 0) + 1 === messageCount ? 'opacity-0' : ''"></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -162,21 +186,21 @@ const messageCount = computed(() => {
|
||||
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)]">
|
||||
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)]">
|
||||
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)]">
|
||||
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>
|
||||
<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)]">
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user