feat: add dropdown for token details
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Todo
|
# Todo
|
||||||
|
|
||||||
- [x] Standardize on one icon set rather than 4 (lmao)
|
- [x] Standardize on one icon set rather than 4 (lmao)
|
||||||
- [-] Make dropdowns a singleton component (work in progress)
|
- [x] Make dropdowns a singleton component (work in progress)
|
||||||
- [ ] Make the sidebar better :kekdoggo:. It's annoying to manage across routes
|
- [ ] Make the sidebar better :kekdoggo:. It's annoying to manage across routes
|
||||||
- [x] Make topics manually renameable
|
- [x] Make topics manually renameable
|
||||||
- [ ] Improve latex rendering, make single line equations work _well_, and likely make it configurable
|
- [ ] Improve latex rendering, make single line equations work _well_, and likely make it configurable
|
||||||
|
|||||||
@@ -5,9 +5,22 @@ import Text from './Text.vue';
|
|||||||
import Tool from './Tool/index.vue';
|
import Tool from './Tool/index.vue';
|
||||||
import type { MessageEntity } from '~/composables/useChat';
|
import type { MessageEntity } from '~/composables/useChat';
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
message: MessageEntity
|
message: MessageEntity
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const triggerRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const { open, close, isOpen } = useTokenDropdown();
|
||||||
|
|
||||||
|
const toggleTokenDropdown = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(props.message.generation!, triggerRef.value!);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -40,7 +53,8 @@ defineProps<{
|
|||||||
{{ message.generation.modelId }}
|
{{ message.generation.modelId }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<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)]">
|
||||||
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
<span class="flex gap-1 items-center" v-if="message.generation.tokens?.output">
|
||||||
<span class="i-tabler-coins"></span>
|
<span class="i-tabler-coins"></span>
|
||||||
{{ message.generation?.tokens?.output }}
|
{{ message.generation?.tokens?.output }}
|
||||||
@@ -49,7 +63,7 @@ defineProps<{
|
|||||||
<span v-if="message.generation.tokens?.tps">
|
<span v-if="message.generation.tokens?.tps">
|
||||||
{{ message.generation.tokens.tps.toFixed(1) }} tps
|
{{ message.generation.tokens.tps.toFixed(1) }} tps
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFloating, offset, flip, shift, autoUpdate } from '@floating-ui/vue';
|
||||||
|
|
||||||
|
const tokenDetailsDropdownRef = ref<HTMLElement | null>(null);
|
||||||
|
const { triggerElement, activeGeneration: generation, close, isOpen } = useTokenDropdown();
|
||||||
|
|
||||||
|
const { floatingStyles } = useFloating(triggerElement, tokenDetailsDropdownRef, {
|
||||||
|
placement: 'bottom-end',
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
middleware: [
|
||||||
|
offset(4),
|
||||||
|
flip(),
|
||||||
|
shift({ padding: 10 }),
|
||||||
|
],
|
||||||
|
transform: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputPercentage = computed(() => {
|
||||||
|
if (!generation.value?.tokens?.output || !generation.value.tokens.thinking) return 0;
|
||||||
|
return (generation.value.tokens.thinking / generation.value.tokens.output) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheReadPercentage = computed(() => {
|
||||||
|
const cache = generation.value?.tokens?.cache;
|
||||||
|
if (!cache || !cache.read || !cache.write) return 0;
|
||||||
|
return (cache.read / (cache.read + cache.write)) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCache = computed(() => {
|
||||||
|
const cache = generation.value?.tokens?.cache;
|
||||||
|
return cache && ((cache.read ?? 0) > 0 || (cache.write ?? 0) > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatNumber = (num?: number) => {
|
||||||
|
if (num === undefined || num === null) return '0';
|
||||||
|
return num.toLocaleString();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-[opacity,transform] duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||||
|
enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100"
|
||||||
|
leave-active-class="transition-[opacity,transform] duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||||
|
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"
|
||||||
|
class="absolute z-50 flex flex-col min-w-64 bg-[var(--bg-surface)] rounded-lg border border-[var(--color-border)] shadow-xl">
|
||||||
|
<div class="p-2 border-b border-[var(--color-border)]">
|
||||||
|
<span class="font-medium">Token Details</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wide">Output</span>
|
||||||
|
<span class="text-sm font-medium">{{ formatNumber(generation.tokens.output ?? 0) }}
|
||||||
|
tokens</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full h-2 mb-3 bg-[var(--bg-hover)] rounded-full overflow-hidden">
|
||||||
|
<div class="absolute left-0 top-0 bottom-0 bg-pink-400"
|
||||||
|
:style="{ width: `${outputPercentage}%` }">
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-0 bottom-0 bg-[var(--text-tertiary)]"
|
||||||
|
:style="{ left: `${outputPercentage}%`, width: `${100 - outputPercentage}%` }">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-6 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 bg-pink-400 rounded-full"></span>
|
||||||
|
<span class="text-[var(--text-secondary)]">Reasoning</span>
|
||||||
|
<span class="font-medium">{{ formatNumber(generation.tokens.thinking ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 bg-[var(--text-tertiary)] rounded-full"></span>
|
||||||
|
<span class="text-[var(--text-secondary)]">Output</span>
|
||||||
|
<span class="font-medium">
|
||||||
|
{{ formatNumber((generation.tokens.output ?? 0) - (generation.tokens.thinking ?? 0)) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasCache" class="px-2 pb-2">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span
|
||||||
|
class="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wide">Cache</span>
|
||||||
|
<span class="text-sm font-medium">{{ formatNumber((generation.tokens.cache?.read || 0) +
|
||||||
|
(generation.tokens.cache?.write || 0)) }} tokens</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full h-2 mb-3 bg-[var(--bg-hover)] rounded-full overflow-hidden">
|
||||||
|
<div class="absolute left-0 top-0 bottom-0 bg-sky-400"
|
||||||
|
:style="{ width: `${cacheReadPercentage}%` }">
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-0 bottom-0 bg-emerald-400"
|
||||||
|
:style="{ left: `${cacheReadPercentage}%`, width: `${100 - cacheReadPercentage}%` }">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-6 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 bg-sky-400 rounded-full"></span>
|
||||||
|
<span class="text-[var(--text-secondary)]">Read</span>
|
||||||
|
<span class="font-medium">{{ formatNumber(generation.tokens.cache?.read ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="w-2 h-2 bg-emerald-400 rounded-full"></span>
|
||||||
|
<span class="text-[var(--text-secondary)]">Write</span>
|
||||||
|
<span class="font-medium">{{ formatNumber(generation.tokens.cache?.write ?? 0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex border-t border-[var(--color-border)]">
|
||||||
|
<div class="flex-1 p-2 text-center border-r border-[var(--color-border)]">
|
||||||
|
<div class="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wide mb-1">TTFT
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium">{{ generation.tokens.ttft?.toFixed(1) || '0' }}ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 p-2 text-center">
|
||||||
|
<div class="text-xs font-medium text-[var(--text-secondary)] uppercase tracking-wide mb-1">Speed
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium">{{ generation.tokens.tps?.toFixed(1) || '0' }} tps</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Entity } from '@triplit/client';
|
||||||
|
import type { schema } from '#triplit/schema';
|
||||||
|
|
||||||
|
type Generation = Entity<typeof schema, 'generations'>;
|
||||||
|
|
||||||
|
export const useTokenDropdown = () => {
|
||||||
|
const isOpen = useState('token-dropdown:open', () => false);
|
||||||
|
const activeGeneration = useState<Generation | null>('token-dropdown:generation', () => null);
|
||||||
|
const triggerElement = useState<HTMLElement | null>('token-dropdown:trigger', () => null);
|
||||||
|
|
||||||
|
const open = (generation: Generation, trigger: HTMLElement) => {
|
||||||
|
console.log("open", generation, trigger);
|
||||||
|
|
||||||
|
activeGeneration.value = generation;
|
||||||
|
triggerElement.value = trigger;
|
||||||
|
isOpen.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
isOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen: readonly(isOpen),
|
||||||
|
activeGeneration: readonly(activeGeneration),
|
||||||
|
triggerElement: readonly(triggerElement),
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -417,4 +417,6 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MessageTokenDetailsDropdown />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user