feat: add better provider support, icons, regen, and a lot more
This commit is contained in:
@@ -1,12 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import ShikiHighlight from '~/components/Markdown/ShikiHighlight.vue';
|
||||
|
||||
defineProps<{
|
||||
error_part: Entity<typeof schema, 'message_parts'>;
|
||||
const props = defineProps<{
|
||||
error: string | null | undefined;
|
||||
}>();
|
||||
|
||||
const code: Ref<string | null> = ref(null);
|
||||
|
||||
if (props.error !== null && props.error !== undefined) {
|
||||
try {
|
||||
code.value = JSON.stringify(JSON.parse(props.error!), null, 2);
|
||||
} catch (e) {
|
||||
code.value = props.error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<span class="text-sm text-[var(--color-error)]">Generation failed</span>
|
||||
<div class="text-sm">
|
||||
<ShikiHighlight :code="code ?? 'An unknown error occurred'" lang="json" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@ const props = defineProps<{
|
||||
const activeTab = ref('input');
|
||||
|
||||
const indicatorStyle = computed(() => {
|
||||
const tabs = ['input', 'output', 'trace'];
|
||||
const tabs = ['input', 'output', 'error', 'trace'];
|
||||
const index = tabs.indexOf(activeTab.value);
|
||||
// Each tab button is ~48px (40px height + 8px gap)
|
||||
const offset = index * 48;
|
||||
@@ -27,19 +27,26 @@ const lineNumberWidth = ref(1);
|
||||
const input: ComputedRef<string> = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'input':
|
||||
if (props.toolCall.input === null) return '';
|
||||
if (props.toolCall.input === null || props.toolCall.input === undefined) return '';
|
||||
if (props.toolCall.input!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.input!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.input!.value;
|
||||
case 'output':
|
||||
if (props.toolCall.output === null) return '';
|
||||
if (props.toolCall.output === null || props.toolCall.output === undefined) return '';
|
||||
if (props.toolCall.output!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.output!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.output!.value;
|
||||
case 'error':
|
||||
if (props.toolCall.error === null || props.toolCall.error === undefined) return '';
|
||||
if (props.toolCall.error!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.error!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.error!.value;
|
||||
case 'trace': {
|
||||
const traceObj: any = { ...props.toolCall };
|
||||
if (traceObj === null) return '';
|
||||
@@ -105,7 +112,7 @@ watch(
|
||||
<div class="w-full border rounded-lg border-[var(--color-highlight)] flex flex-row h-80 overflow-hidden">
|
||||
<div class="flex items-center gap-2 flex-col border-r border-[var(--color-highlight)] p-1 relative shrink-0">
|
||||
<button @click="activeTab = 'input'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'input' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
@@ -114,18 +121,29 @@ watch(
|
||||
</div>
|
||||
Input
|
||||
</button>
|
||||
<button @click="activeTab = 'output'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
activeTab === 'output' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<button :disabled="toolCall.output === undefined || toolCall.output === null" @click="activeTab = 'output'"
|
||||
:class="[
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'output' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:arrow-down-square" class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
Output
|
||||
</button>
|
||||
<button v-if="toolCall.error !== undefined && toolCall.error !== null" @click="activeTab = 'error'" :class="[
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'error' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:danger-triangle" class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
Error
|
||||
</button>
|
||||
<button @click="activeTab = 'trace'" :class="[
|
||||
'hover:bg-[var(--color-highlight)] p-2 rounded-lg flex gap-1 items-center w-full transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
'function-call-tab-selector',
|
||||
activeTab === 'trace' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
@@ -174,4 +192,26 @@ watch(
|
||||
.light #function-call-container>pre>code .line::before {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.function-call-tab-selector {
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
transition-property: color, background-color, border-color,
|
||||
text-decoration-color, fill, stroke;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
|
||||
.function-call-tab-selector:hover:enabled {
|
||||
background-color: var(--color-highlight);
|
||||
}
|
||||
|
||||
.function-call-tab-selector:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -12,28 +12,6 @@ const deubgToolCallOpen = ref(false);
|
||||
const toggleDebugToolCall = () => {
|
||||
deubgToolCallOpen.value = !deubgToolCallOpen.value;
|
||||
};
|
||||
|
||||
const iconName = ref('mynaui:tool');
|
||||
const iconColor = ref('var(--color-subtle)');
|
||||
|
||||
watch(
|
||||
() => props.toolCall.status,
|
||||
(status) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
iconName.value = 'svg-spinners:180-ring-with-bg';
|
||||
break;
|
||||
case 'completed':
|
||||
iconName.value = 'mynaui:tool';
|
||||
break;
|
||||
case 'failed':
|
||||
iconName.value = 'mynaui:x-solid';
|
||||
iconColor.value = '#ff3b3b';
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -44,8 +22,12 @@ watch(
|
||||
<div class="flex items-center gap-1">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--color-neutral)] flex items-center justify-center">
|
||||
<Icon :name="iconName" :style="{ color: iconColor }"
|
||||
<!-- Explicityly avoid setting the name via a reactive value, otherwise you risk corrupting the icon with that name globally -->
|
||||
<Icon v-if="toolCall.status === 'pending'" name="svg-spinners:180-ring-with-bg"
|
||||
class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
<Icon v-else-if="toolCall.status === 'failed'" name="mynaui:x-solid"
|
||||
class="w-3 h-3 text-[#ff3b3b]" />
|
||||
<Icon v-else name="mynaui:tool" class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
</div>
|
||||
{{ toolCall.toolName }}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import ShikiHighlight from '~/components/Markdown/ShikiHighlight.vue';
|
||||
import Error from './Error.vue';
|
||||
import Reasoning from './Reasoning.vue';
|
||||
import Text from './Text.vue';
|
||||
import Tool from './Tool/index.vue';
|
||||
import type { MessageEntity } from '~/composables/useChat';
|
||||
|
||||
defineProps<{
|
||||
message: Readonly<
|
||||
Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null;
|
||||
})[];
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
>;
|
||||
message: MessageEntity
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -28,28 +21,36 @@ defineProps<{
|
||||
Unhandled part type: {{ part.type }} {{ part }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="flex items-center gap-1"
|
||||
v-if="message.generation?.status === 'pending' && (message.parts || []).length === 0">
|
||||
<Icon name="svg-spinners:pulse-2" class="text-4" />
|
||||
<span class="text-sm text-[var(--color-muted)]">
|
||||
Preparing generating...
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div v-else-if="message.generation?.status === 'failed'">
|
||||
<Error :error="message.generation.error" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-between text-zinc-400 dark:text-zinc-600 text-xs"
|
||||
v-if="message.generation && message.generation.status !== 'pending'">
|
||||
<span class="flex items-center gap-1">
|
||||
<ModelIcon :size="12" :model-id="message.generation.modelId" />
|
||||
{{ message.generation.modelId }}
|
||||
</span>
|
||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||
<Icon name="tabler:coins" />
|
||||
{{ message.generation?.tokens?.output }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="message.generation?.status === 'pending' && message.parts.length === 0">
|
||||
<span class="text-sm text-[var(--color-muted)] flex flex-row items-center">
|
||||
<Icon name="svg-spinners:pulse-2" class="text-4" />
|
||||
Preparing generating...
|
||||
</span>
|
||||
</span>
|
||||
<div v-else-if="message.generation?.status === 'failed'">
|
||||
<span class="text-sm text-[var(--color-error)]">Generation failed</span>
|
||||
<div class="text-sm">
|
||||
<ShikiHighlight :code="message.generation.error ?? 'An unknown error occurred'" lang="json" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||
<Icon name="tabler:coins" />
|
||||
{{ message.generation?.tokens?.output }}
|
||||
</span>
|
||||
|
||||
<span v-if="message.generation.tokens?.tps">
|
||||
{{ message.generation.tokens.tps.toFixed(1) }} tps
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import type { Message } from '~/composables/useChat';
|
||||
|
||||
defineProps<{
|
||||
message: Readonly<
|
||||
Entity<typeof schema, 'messages'> & {
|
||||
parts: (Entity<typeof schema, 'message_parts'> & {
|
||||
toolCall: Entity<typeof schema, 'tool_calls'> | null;
|
||||
})[];
|
||||
} & { generation: Entity<typeof schema, 'generations'> | null }
|
||||
>;
|
||||
const triplit = useTriplitClient();
|
||||
|
||||
const { message } = defineProps<{
|
||||
message: Message
|
||||
}>();
|
||||
|
||||
const focusedIndex = computed({
|
||||
get: () => {
|
||||
return message.focusedIndex || 0;
|
||||
},
|
||||
set: (newValue) => {
|
||||
triplit.update('messages', message.id, {
|
||||
focusedIndex: newValue
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => message.children.length, (newCount, oldCount) => {
|
||||
if (newCount === oldCount) return;
|
||||
|
||||
// if we are deleting children, only move the focus index if its no longer valid
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedIndex.value > newCount) {
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
focusedIndex.value = newCount - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
focusedIndex.value = newCount;
|
||||
});
|
||||
|
||||
const activeMessage = computed(() => {
|
||||
if (message.children.length === 0 || (focusedIndex.value === 0 && !message.deleted)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message.deleted === true) {
|
||||
return message.children[Math.min(focusedIndex.value, message.children.length - 1)];
|
||||
}
|
||||
|
||||
return message.children[focusedIndex.value - 1];
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
regenerate: []
|
||||
delete: []
|
||||
}>();
|
||||
|
||||
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 deleteMessage = () => {
|
||||
if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
||||
focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
emit('delete');
|
||||
});
|
||||
};
|
||||
|
||||
const messageCount = computed(() => {
|
||||
if (message.deleted === true) {
|
||||
return message.children.length;
|
||||
}
|
||||
|
||||
return message.children.length + 1;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['max-w-full mb-4', message.role === 'user' ? 'pl-9 flex justify-end' : '']">
|
||||
<MessageUser v-if="message.role === 'user'" :message="message" />
|
||||
<MessageAgent v-else :message="message" />
|
||||
<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">
|
||||
<button :inert="focusedIndex === 0" @click="focusedIndex = focusedIndex! - 1">
|
||||
<Icon name="mynaui:chevron-left" :class="['w-4 h-4', focusedIndex === 0 ? 'opacity-0' : '']" />
|
||||
</button>
|
||||
<span class="text-xs text-[var(--color-muted)]">
|
||||
{{ focusedIndex + 1 }} / {{ messageCount }}
|
||||
</span>
|
||||
<button :inert="focusedIndex + 1 === messageCount" @click="focusedIndex = focusedIndex + 1">
|
||||
<Icon name="mynaui:chevron-right"
|
||||
:class="['w-4 h-4', focusedIndex + 1 === messageCount ? 'opacity-0' : '']" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="activeMessage.generation?.status === 'pending' ? 'opacity-0!' : ''"
|
||||
class="self-end mt-1 w-fit bg-[var(--color-highlight)] text-[var(--color-subtle)] 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)]">
|
||||
<button @click="regenerateMessage"
|
||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-highlight)]">
|
||||
<Icon name="mynaui:refresh" class="text-4.5" />
|
||||
</button>
|
||||
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
|
||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-highlight)]">
|
||||
<Icon :name="copied ? 'mynaui:check' : 'mynaui:copy'" class="text-4.5" />
|
||||
</button>
|
||||
<button @click="deleteMessage"
|
||||
class="flex justify-center items-center w-7 h-6 text-red-500 hover:bg-[var(--color-highlight)]">
|
||||
<Icon name="mynaui:trash" class="text-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user