streaming, markdown, model selecting, and lots more
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
defineProps<{
|
||||
error_part: Entity<typeof schema, 'message_parts'>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<Entity<typeof schema, 'message_parts'>>;
|
||||
}>();
|
||||
|
||||
const reasoningOpen = ref(!props.part.finished);
|
||||
|
||||
watch(
|
||||
() => props.part.finished,
|
||||
() => {
|
||||
reasoningOpen.value = !props.part.finished;
|
||||
},
|
||||
);
|
||||
|
||||
const containerRef: Ref<HTMLDivElement | null> = ref(null);
|
||||
const scrollState = ref('middle');
|
||||
|
||||
const { scrollToBottom } = useAutoScroll(containerRef);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
const container = containerRef.value;
|
||||
const scrollTop = container.scrollTop;
|
||||
const scrollHeight = container.scrollHeight;
|
||||
const clientHeight = container.clientHeight;
|
||||
|
||||
const topThreshold = 0.02 * clientHeight;
|
||||
|
||||
if (scrollTop <= topThreshold) {
|
||||
scrollState.value = 'top';
|
||||
} else if (scrollTop + 100 >= scrollHeight - clientHeight) {
|
||||
scrollState.value = 'bottom';
|
||||
} else {
|
||||
scrollState.value = 'middle';
|
||||
}
|
||||
};
|
||||
|
||||
const toggleReasoning = async () => {
|
||||
if (!props.part.finished) return;
|
||||
|
||||
reasoningOpen.value = !reasoningOpen.value;
|
||||
if (!reasoningOpen.value) return;
|
||||
await nextTick();
|
||||
scrollToBottom('instant');
|
||||
handleScroll();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button @click="toggleReasoning" :class="[
|
||||
'w-full hover:bg-[var(--color-highlight)] rounded-lg p-1 flex justify-between items-center text-[--color-reasoning] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]',
|
||||
part.finished ? '' : 'cursor-default'
|
||||
]">
|
||||
<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="mynaui:atom" class="w-3 h-3 text-[var(--reasoning-accent)]" />
|
||||
</div>
|
||||
Deep Thinking
|
||||
</div>
|
||||
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', reasoningOpen ? '' : '-rotate-90']" />
|
||||
</button>
|
||||
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll"
|
||||
:class="['reasoning-contaizner p-2 text-[--color-reasoning] max-h-[min(40vh,320px)] overflow-y-auto', scrollState]">
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.reasoning-contaizner.middle {
|
||||
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent)
|
||||
}
|
||||
|
||||
.reasoning-contaizner.top {
|
||||
mask-image: linear-gradient(#000, transparent, #000 0, #000 12%, #000 88%, transparent)
|
||||
}
|
||||
|
||||
.reasoning-contaizner.bottom {
|
||||
mask-image: linear-gradient(transparent, #000, transparent 0, #000 12%, #000 88%, #000)
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
const props = defineProps<{
|
||||
part: Readonly<Entity<typeof schema, 'message_parts'>>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MarkdownRenderer :finished="part.finished" :id="part.id" :content="part.content" />
|
||||
</template>
|
||||
@@ -0,0 +1,177 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<Entity<typeof schema, 'tool_calls'>>;
|
||||
}>();
|
||||
|
||||
const activeTab = ref('input');
|
||||
|
||||
const indicatorStyle = computed(() => {
|
||||
const tabs = ['input', 'output', 'trace'];
|
||||
const index = tabs.indexOf(activeTab.value);
|
||||
// Each tab button is ~48px (40px height + 8px gap)
|
||||
const offset = index * 48;
|
||||
return {
|
||||
transform: `translateY(${offset}px)`,
|
||||
top: '4px',
|
||||
};
|
||||
});
|
||||
|
||||
const shiki = await getShikiHighlighter();
|
||||
|
||||
const html = ref('');
|
||||
const lineNumberWidth = ref(1);
|
||||
|
||||
const input: ComputedRef<string> = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'input':
|
||||
if (props.toolCall.input === null) 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!.type === 'json') {
|
||||
return JSON.stringify(JSON.parse(props.toolCall.output!.value), null, 2);
|
||||
}
|
||||
|
||||
return props.toolCall.output!.value;
|
||||
case 'trace': {
|
||||
const traceObj: any = { ...props.toolCall };
|
||||
if (traceObj === null) return '';
|
||||
// marshall the trace object and the input, output, and error into their correct types
|
||||
switch (traceObj.input?.type) {
|
||||
case 'text':
|
||||
traceObj.input = traceObj.input.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.input = JSON.parse(traceObj.input.value);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (traceObj.output?.type) {
|
||||
case 'text':
|
||||
traceObj.output = traceObj.output.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.output = JSON.parse(traceObj.output.value);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (traceObj.error?.type) {
|
||||
case 'text':
|
||||
traceObj.error = traceObj.error.value;
|
||||
break;
|
||||
case 'json':
|
||||
traceObj.error = JSON.parse(traceObj.error.value);
|
||||
break;
|
||||
}
|
||||
|
||||
return JSON.stringify(traceObj, null, 2);
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
input,
|
||||
(newCode) => {
|
||||
let lang = 'json';
|
||||
try {
|
||||
shiki.getLanguage(lang);
|
||||
} catch (e) {
|
||||
lang = 'text';
|
||||
}
|
||||
|
||||
html.value = shiki.codeToHtml(newCode, {
|
||||
lang,
|
||||
themes: {
|
||||
dark: 'vitesse-dark',
|
||||
light: 'vitesse-light',
|
||||
},
|
||||
});
|
||||
lineNumberWidth.value = html.value.split('\n').length.toString().length;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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)]',
|
||||
activeTab === 'input' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:code" class="w-4.5 h-4.5" />
|
||||
</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' : ''
|
||||
]">
|
||||
<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 @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)]',
|
||||
activeTab === 'trace' ? 'text-orange-6' : ''
|
||||
]">
|
||||
<div
|
||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden flex items-center justify-center">
|
||||
<Icon name="mynaui:flask" class="w-4.5 h-4.5" />
|
||||
</div>
|
||||
Function call
|
||||
</button>
|
||||
|
||||
<div :style="indicatorStyle"
|
||||
class="absolute bg-orange-6 w-[3px] h-8 rounded-l mt-1 right-0 transition-transform duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow overflow-auto max-h-full max-w-full">
|
||||
<div class="overflow-hidden h-full" :style="`--line-number-width: ${lineNumberWidth}ch`"
|
||||
id="function-call-container" v-html="html">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#function-call-container>pre {
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding: 1rem;
|
||||
line-height: 1.625;
|
||||
counter-reset: lines;
|
||||
}
|
||||
|
||||
#function-call-container>pre>code .line::before {
|
||||
counter-increment: lines;
|
||||
content: counter(lines);
|
||||
width: var(--line-number-width);
|
||||
margin-right: 1.5rem;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dark #function-call-container>pre>code .line::before {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.light #function-call-container>pre>code .line::before {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import Debug from './Debug.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
toolCall: Readonly<Entity<typeof schema, 'tool_calls'>>;
|
||||
}>();
|
||||
|
||||
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>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="select-none w-full hover:bg-[var(--color-highlight)] group rounded-lg p-1 flex justify-between items-center text-[--color-reasoning] 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
|
||||
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 }"
|
||||
class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
</div>
|
||||
{{ toolCall.toolName }}
|
||||
</div>
|
||||
|
||||
<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-highlight)] flex items-center justify-center transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||
<Icon name="mynaui:search" class="w-3 h-3 text-[var(--color-subtle)]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Debug v-if="deubgToolCallOpen" :toolCall="toolCall" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { Entity } from '@triplit/client';
|
||||
import type schema from '#triplit/schema';
|
||||
import ShikiHighlight from '~/components/Markdown/ShikiHighlight.vue';
|
||||
import Reasoning from './Reasoning.vue';
|
||||
import Text from './Text.vue';
|
||||
import Tool from './Tool/index.vue';
|
||||
|
||||
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 }
|
||||
>;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <pre class="max-w-full overflow-x-auto">{{ JSON.stringify(message, null, 2) }}</pre> -->
|
||||
<div class="flex flex-col w-full gap-2">
|
||||
<div v-for="part in message.parts" :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!" />
|
||||
<div v-else>
|
||||
Unhandled part type: {{ part.type }} {{ part }}
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user