♻️ refactor: optimize state management, switch to @tanstack/vue-virtual, and improve performance
- Centralize `useAgents` and `useModels` state within the Nuxt app context to prevent data leaks and improve initialization. - Migrate virtualization from `vue-virtual-scroller` to `@tanstack/vue-virtual` with new `RowVirtualizerFixed` and `RowVirtualizerDynamic` components. - Upgrade Nuxt to v4.3.1 and remove `@vue-macros/nuxt`. - Replace `big.js` with an optimized custom `lshDecimal` string manipulation logic for pricing calculations in the provider API. - Implement automatic focus redirection in `ChatInput` to capture standard keyboard input. - Refactor Sidenav and Settings components to utilize virtualization for long lists (topics, agents, models). - Enhance theme colors and mobile experience. More work to come on both of these.
This commit is contained in:
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Run vite",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"dependi.npm.lockFileEnabled": true
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|
||||||
// import 'katex/dist/katex.min.css'
|
|
||||||
import '~/assets/css/reset.css';
|
import '~/assets/css/reset.css';
|
||||||
import '~/assets/css/base.css';
|
import '~/assets/css/base.css';
|
||||||
|
|
||||||
|
|||||||
+14
-19
@@ -57,15 +57,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root.dark {
|
:root.dark {
|
||||||
--palette-zinc-100: #09080A;
|
--palette-zinc-100: #09080D;
|
||||||
--palette-zinc-200: #1a191b;
|
--palette-zinc-200: #1a191E;
|
||||||
--palette-zinc-300: #2e2d2f;
|
--palette-zinc-300: #2e2d32;
|
||||||
--palette-slate-100: #0f171c;
|
--palette-slate-100: #0f171c;
|
||||||
--palette-slate-200: #1e252b;
|
--palette-slate-200: #1e252b;
|
||||||
--palette-slate-300: #333b44;
|
--palette-slate-300: #333b44;
|
||||||
--palette-obsidian-100: #050504;
|
--palette-obsidian-100: #050307;
|
||||||
--palette-obsidian-200: #0f0f0e;
|
--palette-obsidian-200: #0f0e11;
|
||||||
--palette-obsidian-300: #1e1f1f;
|
--palette-obsidian-300: #1e1d20;
|
||||||
|
|
||||||
--text-primary: color-mix(in srgb, #fafafa, var(--color-accent) var(--accent-hinting));
|
--text-primary: color-mix(in srgb, #fafafa, var(--color-accent) var(--accent-hinting));
|
||||||
/* Selected topics */
|
/* Selected topics */
|
||||||
@@ -177,6 +177,8 @@ html.light {
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
/* something something progressive enhancement */
|
||||||
|
height: 100dvh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +272,11 @@ button.accent:hover {
|
|||||||
transform: translateX(calc(2.5em - 1.3em - 0.5rem));
|
transform: translateX(calc(2.5em - 1.3em - 0.5rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(.i-tabler\:dots) {
|
||||||
|
background-color: currentColor;
|
||||||
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='18' height='18'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
.reasoning-contaizner.middle {
|
.reasoning-contaizner.middle {
|
||||||
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent)
|
mask-image: linear-gradient(#000, #000, transparent 0, #000 12%, #000 88%, transparent)
|
||||||
}
|
}
|
||||||
@@ -465,16 +472,4 @@ article .checkbox {
|
|||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* end markdown renderer */
|
/* end markdown renderer */
|
||||||
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
import type { ModelWithProvider, ProviderWithModels } from '~/composables/useModels';
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Agent } from '~/composables/useAgents';
|
||||||
import { schema } from '#triplit/schema';
|
|
||||||
|
|
||||||
const { allModels } = await useModels();
|
const { allModels } = useModels();
|
||||||
|
|
||||||
const inputHeight: Ref<string> = ref('auto');
|
const inputHeight: Ref<string> = ref('auto');
|
||||||
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
const inputRef = ref<HTMLTextAreaElement | null>(null);
|
||||||
@@ -19,7 +18,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
agent?: Entity<typeof schema, 'agents'>;
|
agent: Agent | null;
|
||||||
providers?: ProviderWithModels[];
|
providers?: ProviderWithModels[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -107,6 +106,15 @@ const handleKeyDown = async (event: KeyboardEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWindowKeyDown = async (event: KeyboardEvent) => {
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect all standard keyboard input to the input field
|
||||||
|
inputRef.value?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
watch(inputValue, async () => {
|
watch(inputValue, async () => {
|
||||||
const textarea = inputRef.value;
|
const textarea = inputRef.value;
|
||||||
if (!textarea) return;
|
if (!textarea) return;
|
||||||
@@ -141,6 +149,11 @@ onBeforeMount(() => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
inputValue.value = tempInput;
|
inputValue.value = tempInput;
|
||||||
|
document.addEventListener('keydown', handleWindowKeyDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleWindowKeyDown);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,15 @@ const renderNode = (node: any, index: number): any => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
defineRender(() => {
|
const render = () => {
|
||||||
const children = ast.value?.children?.flatMap(renderNode) || [];
|
const children = ast.value?.children?.flatMap(renderNode) || [];
|
||||||
|
|
||||||
return h('div', { class: 'prose-wrapper' }, [
|
return h('div', { class: 'prose-wrapper' }, [
|
||||||
h('article', { class: 'markdown-body' }, children)
|
h('article', { class: 'markdown-body' }, children)
|
||||||
]);
|
]);
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<render />
|
||||||
|
</template>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import { type Grammar } from 'shiki';
|
import { type Grammar } from 'shiki';
|
||||||
import { hashSync } from '~/utils/hash';
|
import { hashSync } from '~/utils/hash';
|
||||||
const props = defineProps<{ code: string; language: string }>();
|
const props = defineProps<{ code: string; language: string }>();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Entity } from '@triplit/client';
|
||||||
import type schema from '#triplit/schema';
|
import type schema from '#triplit/schema';
|
||||||
|
|
||||||
@@ -56,13 +56,13 @@ const toggleReasoning = async () => {
|
|||||||
'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'
|
part.finished ? '' : 'cursor-default'
|
||||||
]">
|
]">
|
||||||
<div class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<div
|
<span
|
||||||
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
|
class="w-[24px] h-[24px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center">
|
||||||
<Icon name="mynaui:atom" class="w-3 h-3 text-[var(--reasoning-accent)]" />
|
<Icon name="mynaui:atom" class="w-3 h-3 text-[var(--reasoning-accent)]" />
|
||||||
</div>
|
</span>
|
||||||
Deep Thinking
|
Deep Thinking
|
||||||
</div>
|
</span>
|
||||||
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', reasoningOpen ? '' : '-rotate-90']" />
|
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', reasoningOpen ? '' : '-rotate-90']" />
|
||||||
</button>
|
</button>
|
||||||
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll"
|
<div v-if="reasoningOpen" ref="containerRef" @scroll="handleScroll"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Entity } from '@triplit/client';
|
||||||
import type schema from '#triplit/schema';
|
import type schema from '#triplit/schema';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Entity } from '@triplit/client';
|
||||||
import type schema from '#triplit/schema';
|
import type schema from '#triplit/schema';
|
||||||
import Debug from './Debug.vue';
|
import Debug from './Debug.vue';
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 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" :key="part.id">
|
||||||
<Reasoning v-if="part.type === 'reasoning'" :part="part" />
|
<Reasoning v-if="part.type === 'reasoning'" :part="part" />
|
||||||
@@ -36,10 +35,10 @@ defineProps<{
|
|||||||
|
|
||||||
<div class="flex flex-row justify-between text-[var(--text-tertiary)] text-xs"
|
<div class="flex flex-row justify-between text-[var(--text-tertiary)] text-xs"
|
||||||
v-if="message.generation && message.generation.status !== 'pending'">
|
v-if="message.generation && message.generation.status !== 'pending'">
|
||||||
<span class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<ModelIcon :size="12" :model-id="message.generation.modelId" />
|
<ModelIcon :size="12" :model-id="message.generation.modelId" />
|
||||||
{{ message.generation.modelId }}
|
{{ message.generation.modelId }}
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<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">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Entity } from '@triplit/client';
|
||||||
import type schema from '#triplit/schema';
|
import type schema from '#triplit/schema';
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const deleteModel = async () => {
|
|||||||
<span v-if="model.cost.request && model.cost.request !== '0'"
|
<span v-if="model.cost.request && model.cost.request !== '0'"
|
||||||
class="text-xs whitespace-nowrap flex items-center">
|
class="text-xs whitespace-nowrap flex items-center">
|
||||||
<span class="w-1 h-1 rounded-full bg-[var(--text-secondary)] inline-block mr-1"></span>
|
<span class="w-1 h-1 rounded-full bg-[var(--text-secondary)] inline-block mr-1"></span>
|
||||||
{{ model.cost.request }}/request
|
${{ model.cost.request }}/K request
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,8 +192,8 @@ useClickOutside(dropdownRef, closeDropdown);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="dropdownRef" class="relative">
|
<div ref="dropdownRef" class="relative">
|
||||||
<button @click="isOpen = !isOpen" ref="dropdownButton"
|
<div role="button" @click="isOpen = !isOpen" ref="dropdownButton"
|
||||||
class="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-200"
|
class="cursor-pointer flex items-center w-fit select-none gap-2 px-3 py-1.5 rounded-lg text-sm font-medium transition-colors duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
isOpen
|
isOpen
|
||||||
? 'bg-[var(--color-hover)] text-[var(--text-primary)]'
|
? 'bg-[var(--color-hover)] text-[var(--text-primary)]'
|
||||||
@@ -206,7 +206,7 @@ useClickOutside(dropdownRef, closeDropdown);
|
|||||||
</span>
|
</span>
|
||||||
<Icon name="mynaui:chevron-down" class="text-3.5 transition-transform duration-200"
|
<Icon name="mynaui:chevron-down" class="text-3.5 transition-transform duration-200"
|
||||||
:class="{ 'rotate-180': isOpen }" />
|
:class="{ 'rotate-180': isOpen }" />
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Transition enter-active-class="transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
<Transition enter-active-class="transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: any[];
|
||||||
|
keyField?: string;
|
||||||
|
scrollElement: HTMLElement | null;
|
||||||
|
minItemSize: number;
|
||||||
|
overscan: number;
|
||||||
|
prerender?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtualizer(computed(() => ({
|
||||||
|
count: props.items.length,
|
||||||
|
getScrollElement: () => props.scrollElement,
|
||||||
|
estimateSize: () => props.minItemSize,
|
||||||
|
overscan: props.overscan,
|
||||||
|
getItemKey: (index: number) => props.keyField ? props.items[index]?.[props.keyField] || index : index,
|
||||||
|
initialRect: {
|
||||||
|
width: 0,
|
||||||
|
height: props.prerender ? props.minItemSize * props.prerender : 0
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
|
||||||
|
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems());
|
||||||
|
const totalSize = computed(() => rowVirtualizer.value.getTotalSize());
|
||||||
|
|
||||||
|
const measureElement = (el: Element) => {
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowVirtualizer.value.measureElement(el)
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.items, () => {
|
||||||
|
rowVirtualizer.value.measure();
|
||||||
|
}, { deep: false });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :style="{ height: `${totalSize}px`, width: '100%', position: 'relative' }">
|
||||||
|
<div v-for="virtualRow in virtualRows" :ref="(el) => measureElement(el as Element)"
|
||||||
|
:key="(virtualRow.key as any | number)" :data-index="virtualRow.index" :style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: `${virtualRow.size}`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}">
|
||||||
|
<slot :item="props.items[virtualRow.index]" :index="virtualRow.index" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
items: any[];
|
||||||
|
keyField?: string;
|
||||||
|
scrollElement: HTMLElement | null;
|
||||||
|
itemSize: number;
|
||||||
|
overscan: number;
|
||||||
|
prerender?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rowVirtualizer = useVirtualizer(computed(() => ({
|
||||||
|
count: props.items.length,
|
||||||
|
getScrollElement: () => props.scrollElement,
|
||||||
|
estimateSize: () => props.itemSize,
|
||||||
|
overscan: props.overscan,
|
||||||
|
getItemKey: (index: number) => props.keyField ? props.items[index]?.[props.keyField] || index : index,
|
||||||
|
initialRect: {
|
||||||
|
width: 0,
|
||||||
|
height: props.prerender ? props.itemSize * props.prerender : 0
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
|
||||||
|
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems());
|
||||||
|
const totalSize = computed(() => rowVirtualizer.value.getTotalSize());
|
||||||
|
|
||||||
|
watch(() => props.items, () => {
|
||||||
|
rowVirtualizer.value.measure();
|
||||||
|
}, { deep: false });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :style="{ height: `${totalSize}px`, width: '100%', position: 'relative' }">
|
||||||
|
<div v-for="virtualRow in virtualRows" :key="(virtualRow.key as any | number)" :style="{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: `${virtualRow.size}`,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}">
|
||||||
|
<slot :item="props.items[virtualRow.index]" :index="virtualRow.index" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -4,12 +4,13 @@ import { encryptData, decrypt, uint8ArrayToBase64, base64ToUint8Array } from '~/
|
|||||||
import { providerBaseUrls, type Model } from '~/types/model';
|
import { providerBaseUrls, type Model } from '~/types/model';
|
||||||
import { useSettings } from '~/composables/useSettings';
|
import { useSettings } from '~/composables/useSettings';
|
||||||
import ModelItem from './ModelItem.vue';
|
import ModelItem from './ModelItem.vue';
|
||||||
// @ts-ignore
|
import RowVirtualizerDynamic from '../RowVirtualizerDynamic.vue';
|
||||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const { pageParams } = useSettings();
|
const { pageParams } = useSettings();
|
||||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers } = useModels();
|
||||||
|
|
||||||
|
const scrollContainerRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const provider = computed(() => {
|
const provider = computed(() => {
|
||||||
if (pageParams.value.length === 0) return null;
|
if (pageParams.value.length === 0) return null;
|
||||||
@@ -29,9 +30,16 @@ const modelSearch = ref('');
|
|||||||
|
|
||||||
watch(pageParams, () => {
|
watch(pageParams, () => {
|
||||||
if (pageParams.value.length === 0) return;
|
if (pageParams.value.length === 0) return;
|
||||||
|
console.log(scrollContainerRef.value);
|
||||||
apiKey.value = provider.value?.config.apiKey ?? '';
|
apiKey.value = provider.value?.config.apiKey ?? '';
|
||||||
apiProxyUrl.value = provider.value?.config.apiProxyUrl ?? '';
|
apiProxyUrl.value = provider.value?.config.apiProxyUrl ?? '';
|
||||||
modelSearch.value = '';
|
modelSearch.value = '';
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (scrollContainerRef.value) {
|
||||||
|
scrollContainerRef.value.scrollTo({ top: 0, behavior: 'instant' });
|
||||||
|
}
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
const decryptApiKey = async () => {
|
const decryptApiKey = async () => {
|
||||||
@@ -142,7 +150,8 @@ const fetchModels = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// delete models that are not in the API response and are not custom models
|
// delete models that are not in the API response and are not custom models
|
||||||
const apiModelIds = new Set(existingModelsMap.values().map((m: any) => m.externalId));
|
const apiModelIds = new Set(modelsData.models.values().map((m: any) => m.id));
|
||||||
|
console.log("apiModelIds", apiModelIds);
|
||||||
const toDelete = (provider.value?.models || []).filter((m: any) =>
|
const toDelete = (provider.value?.models || []).filter((m: any) =>
|
||||||
!m.isCustom && !apiModelIds.has(m.externalId)
|
!m.isCustom && !apiModelIds.has(m.externalId)
|
||||||
);
|
);
|
||||||
@@ -177,15 +186,13 @@ const disabledModels = computed(() =>
|
|||||||
.sort(sortByReleaseDate)
|
.sort(sortByReleaseDate)
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(['navigate']);
|
defineEmits(['navigate']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 mt-4" v-if="provider">
|
<div ref="scrollContainerRef"
|
||||||
|
class="flex flex-col gap-4 py-4 overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]"
|
||||||
|
v-if="provider">
|
||||||
<div class="flex flex-row justify-between gap-16">
|
<div class="flex flex-row justify-between gap-16">
|
||||||
<label class="whitespace-nowrap" for="provider-api-key">Enabled</label>
|
<label class="whitespace-nowrap" for="provider-api-key">Enabled</label>
|
||||||
<Slider :checked="provider.enabled" @click.stop="toggleProvider()" />
|
<Slider :checked="provider.enabled" @click.stop="toggleProvider()" />
|
||||||
@@ -208,7 +215,7 @@ defineEmits(['navigate']);
|
|||||||
<div class="flex flex-row justify-between gap-16">
|
<div class="flex flex-row justify-between gap-16">
|
||||||
<label class="whitespace-nowrap" for="provider-api-key">API Proxy URL</label>
|
<label class="whitespace-nowrap" for="provider-api-key">API Proxy URL</label>
|
||||||
<div class="text-sm font-mono flex flex-row rounded-md bg-[var(--bg-container)] items-center gap-1 w-7/10">
|
<div class="text-sm font-mono flex flex-row rounded-md bg-[var(--bg-container)] items-center gap-1 w-7/10">
|
||||||
<input :placeholder="providerBaseUrls[provider!.type] ?? ''"
|
<input :placeholder="provider.type ? providerBaseUrls[provider.type] ?? '' : ''"
|
||||||
class="placeholder:text-[var(--text-tertiary)] w-full px-2 py-1 bg-transparent" type="text"
|
class="placeholder:text-[var(--text-tertiary)] w-full px-2 py-1 bg-transparent" type="text"
|
||||||
id="provider-proxy-url" :value="apiProxyUrl"
|
id="provider-proxy-url" :value="apiProxyUrl"
|
||||||
@input="updateProxyUrl(($event.target! as HTMLInputElement).value)" />
|
@input="updateProxyUrl(($event.target! as HTMLInputElement).value)" />
|
||||||
@@ -266,37 +273,24 @@ defineEmits(['navigate']);
|
|||||||
Enabled
|
Enabled
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
<RowVirtualizerDynamic :items="enabledModels" key-field="id"
|
||||||
:items="enabledModels" key-field="id">
|
:scroll-element="scrollContainerRef" :min-item-size="64" :overscan="20">
|
||||||
<template v-slot="{ item: model, index, active }">
|
<template v-slot="{ item: model }">
|
||||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
<ModelItem :model="model" />
|
||||||
model.name,
|
|
||||||
model.externalId,
|
|
||||||
modelSearch
|
|
||||||
]" :data-index="index">
|
|
||||||
<ModelItem :model="model" />
|
|
||||||
</DynamicScrollerItem>
|
|
||||||
</template>
|
</template>
|
||||||
</DynamicScroller>
|
</RowVirtualizerDynamic>
|
||||||
|
</div>
|
||||||
|
|
||||||
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--text-secondary)]">
|
<span v-if="disabledModels.length > 0" class="text-sm text-[var(--text-secondary)]">
|
||||||
Disabled
|
Disabled
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<DynamicScroller class="scroller" page-mode :min-item-size="64" :buffer="640"
|
<RowVirtualizerDynamic :items="disabledModels" key-field="id"
|
||||||
:items="disabledModels" key-field="id">
|
:scroll-element="scrollContainerRef" :min-item-size="64" :overscan="20">
|
||||||
<template v-slot="{ item: model, index, active }">
|
<template v-slot="{ item: model }">
|
||||||
<DynamicScrollerItem :item="model" :active="active" :size-dependencies="[
|
<ModelItem :model="model" />
|
||||||
model.name,
|
</template>
|
||||||
model.externalId,
|
</RowVirtualizerDynamic>
|
||||||
model.cost,
|
|
||||||
modelSearch
|
|
||||||
]" :data-index="index">
|
|
||||||
<ModelItem :model="model" />
|
|
||||||
</DynamicScrollerItem>
|
|
||||||
</template>
|
|
||||||
</DynamicScroller>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ defineEmits(['navigate']);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-6 mt-4">
|
<div
|
||||||
|
class="flex flex-col gap-6 py-4 overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]">
|
||||||
<div class="flex flex-row items-center justify-between gap-2">
|
<div class="flex flex-row items-center justify-between gap-2">
|
||||||
<h4 class="font-medium">Theme</h4>
|
<h4 class="font-medium">Theme</h4>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import SystemAssistants from './SystemAssistants.vue';
|
|||||||
import AppearanceSettings from './AppearanceSettings.vue';
|
import AppearanceSettings from './AppearanceSettings.vue';
|
||||||
import AIServiceProvider from './AIServiceProvider.vue';
|
import AIServiceProvider from './AIServiceProvider.vue';
|
||||||
|
|
||||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers } = useModels();
|
||||||
|
|
||||||
const PAGES_CONFIG = {
|
const PAGES_CONFIG = {
|
||||||
general: {
|
general: {
|
||||||
@@ -81,7 +81,6 @@ onUnmounted(() => {
|
|||||||
if (open.value) {
|
if (open.value) {
|
||||||
document.body.removeEventListener('keydown', handleKeyDown);
|
document.body.removeEventListener('keydown', handleKeyDown);
|
||||||
}
|
}
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -114,8 +113,8 @@ onUnmounted(() => {
|
|||||||
<!-- DYNAMIC CONTENT -->
|
<!-- DYNAMIC CONTENT -->
|
||||||
<main class="flex-1 flex flex-col overflow-hidden">
|
<main class="flex-1 flex flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col flex-1 p-3 bg-[var(--bg-surface)] overflow-y-auto border rounded-lg border-[var(--color-border)]">
|
class="max-h-full h-full flex flex-col flex-1 px-3 bg-[var(--bg-surface)] border rounded-lg border-[var(--color-border)]">
|
||||||
<header class="flex items-center justify-between pl-2 pb-2 ">
|
<header class="flex items-center justify-between pl-2 pb-2 pt-2 ">
|
||||||
<h2 class="text-lg font-semibold m-0 capitalize">{{ runtimePage.label }}</h2>
|
<h2 class="text-lg font-semibold m-0 capitalize">{{ runtimePage.label }}</h2>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-[var(--color-hover)] p-1.5 rounded-md transition-colors duration-200 ease-[cubic-bezier(0,0.55,0.45,1)]"
|
class="hover:bg-[var(--color-hover)] p-1.5 rounded-md transition-colors duration-200 ease-[cubic-bezier(0,0.55,0.45,1)]"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script setup lang="ts">
|
||||||
const props = defineProps<{
|
const props = defineProps<{ model: ModelWithProvider; }>();
|
||||||
model: ModelWithProvider;
|
|
||||||
}>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Providers } from '~/types/model';
|
|||||||
import { providerIcons } from '~/utils/model-mapping';
|
import { providerIcons } from '~/utils/model-mapping';
|
||||||
|
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers } = useModels();
|
||||||
|
|
||||||
if (providers.value === undefined) throw new Error('Providers not loaded');
|
if (providers.value === undefined) throw new Error('Providers not loaded');
|
||||||
|
|
||||||
@@ -37,15 +37,12 @@ const toggleProvider = async (id: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(['navigate']);
|
defineEmits(['navigate']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-1">
|
<div
|
||||||
|
class="flex flex-col gap-1 py-4 overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]">
|
||||||
<h2 class="text-lg font-semibold flex items-center gap-2">
|
<h2 class="text-lg font-semibold flex items-center gap-2">
|
||||||
Enabled <span class="text-sm bg-[var(--bg-container)] px-2 rounded-md py-0.5 text-[var(--text-secondary)]">
|
Enabled <span class="text-sm bg-[var(--bg-container)] px-2 rounded-md py-0.5 text-[var(--text-secondary)]">
|
||||||
{{providers?.filter(p => p.enabled).length}}
|
{{providers?.filter(p => p.enabled).length}}
|
||||||
|
|||||||
@@ -2,17 +2,13 @@
|
|||||||
import { providerIcons } from '~/utils/model-mapping';
|
import { providerIcons } from '~/utils/model-mapping';
|
||||||
|
|
||||||
const { pageParams } = useSettings();
|
const { pageParams } = useSettings();
|
||||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers } = await useModels();
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
defineEmits(['navigate']);
|
defineEmits(['navigate']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1 overflow-auto">
|
||||||
<button @click="$emit('navigate', 'general')"
|
<button @click="$emit('navigate', 'general')"
|
||||||
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
class="flex items-center gap-2 p-2 rounded-lg text-sm hover:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
<Icon name="mynaui:chevron-left" class="text-4" /> Back to General
|
<Icon name="mynaui:chevron-left" class="text-4" /> Back to General
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ defineEmits(['navigate']);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 flex-grow">
|
<div
|
||||||
|
class="flex flex-col gap-4 py-4 flex-grow overflow-auto [scrollbar-width:thin] [scrollbar-color:#888_transparent] [scrollbar-gutter:stable]">
|
||||||
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants" :key="key">
|
<div class="flex flex-col" v-for="(systemAssistant, key) in settings.systemAssistants" :key="key">
|
||||||
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center">
|
<label :for="`slider-${key}`" class="flex justify-between gap-4 items-center">
|
||||||
<h4 class="capitalize">{{ key }}</h4>
|
<h4 class="capitalize">{{ key }}</h4>
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { agents, getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
const { agents, getAgent } = useAgents();
|
||||||
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
|
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
|
||||||
|
|
||||||
const activeAgent = computed(() => getAgent(route.params.id as string));
|
const activeAgent = getAgent(route.params.id as string);
|
||||||
|
|
||||||
const { isHovered, sidebarWidth } = useSidenavContext();
|
const { isHovered } = useSidenavContext();
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeAgents?.();
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleDropdown = (e: MouseEvent) => {
|
const toggleDropdown = (e: MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -66,36 +62,5 @@ const toggleDropdown = (e: MouseEvent) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- <Dropdown class="overflow-hidden" v-model="agentDropdownOpen" placement="center" width="calc(80% - 1rem)">
|
|
||||||
<template #trigger="{ toggle }">
|
|
||||||
<button
|
|
||||||
class="flex max-w-full transition duration-200 gap-1.5 pr-2 items-center cursor-pointer hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg"
|
|
||||||
@click="toggle">
|
|
||||||
<div
|
|
||||||
:class="['w-[28px] h-[28px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--bg-surface)] flex items-center justify-center', activeAgent?.imageUrl ? '' : 'border border-[var(--color-border)]']">
|
|
||||||
<img v-if="activeAgent?.imageUrl" :src="activeAgent.imageUrl"
|
|
||||||
class="w-full h-full object-cover" />
|
|
||||||
<Icon v-else name="mynaui:check-hexagon" class="w-4 h-4 text-[var(--color-accent)]" />
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--text-primary)] whitespace-nowrap">
|
|
||||||
{{ activeAgent?.name }}
|
|
||||||
</span>
|
|
||||||
<div class="w-4 h-4 text-[var(--text-secondary)]">
|
|
||||||
<Icon
|
|
||||||
class="text-4 transform-origin-center-left duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)] transition-all"
|
|
||||||
name="mynaui:chevron-up-down" />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div class="flex flex-col gap-1.5 max-h-[calc(2.25rem*4+0.375rem*3)] overflow-y-auto">
|
|
||||||
<SidenavItem draggable="false" v-for="agent in agents" :to="`/agent/${agent.id}`" :name="agent.name"
|
|
||||||
class="whitespace-nowrap" icon="mynaui:check-hexagon" :key="agent.id"
|
|
||||||
:active="activeAgent?.id === agent.id" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dropdown> -->
|
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,18 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { assert } from '~~/utils/assert';
|
import { assert } from '~~/utils/assert';
|
||||||
|
import RowVirtualizerFixed from '~/components/RowVirtualizerFixed.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
|
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
|
||||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
const { getAgent } = useAgents();
|
||||||
|
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const activeAgent = computed(() => getAgent(route.params.id as string));
|
const navRef = ref<HTMLElement | null>(null);
|
||||||
|
const activeAgent = getAgent(route.params.id as string);
|
||||||
|
watch(() => route.params.id, () => {
|
||||||
|
if (navRef.value) {
|
||||||
|
navRef.value.scrollTo({ top: 0, behavior: 'instant' });
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const topics = computed(() => {
|
const topics = computed(() => {
|
||||||
if (activeAgent.value === undefined) return [];
|
return activeAgent.value?.topics || [];
|
||||||
return activeAgent.value.topics;
|
})
|
||||||
});
|
|
||||||
|
|
||||||
const topicsOpen = ref(true);
|
const topicsOpen = ref(true);
|
||||||
let activeAutoRenames = reactive(new Map<string, string>());
|
let activeAutoRenames = reactive(new Map<string, string>());
|
||||||
@@ -116,54 +122,33 @@ const handleNavClick = (e: MouseEvent) => {
|
|||||||
if (!topicId) return;
|
if (!topicId) return;
|
||||||
|
|
||||||
const action = trigger.dataset.action;
|
const action = trigger.dataset.action;
|
||||||
if (action === 'navigate') {
|
switch (action) {
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey) return;
|
case 'navigate':
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
return navigateTo(`/agent/${route.params.id}/topic/${topicId}`);
|
||||||
|
|
||||||
e.preventDefault();
|
case 'toggle-dropdown':
|
||||||
return navigateTo(`/agent/${route.params.id}/topic/${topicId}`);
|
e.preventDefault();
|
||||||
}
|
e.stopPropagation();
|
||||||
|
if (dropdownState.open) {
|
||||||
if (action === 'toggle-dropdown') {
|
closeDropdown();
|
||||||
e.preventDefault();
|
return;
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (dropdownState.open) {
|
|
||||||
closeDropdown();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemsFactory = () => {
|
|
||||||
const items = [];
|
|
||||||
const isRenaming = activeAutoRenames.has(topicId) && topics.value?.find(t => t.id === topicId)?.renaming;
|
|
||||||
|
|
||||||
if (isRenaming) {
|
|
||||||
items.push({
|
|
||||||
label: 'Cancel Auto Rename',
|
|
||||||
onClick: () => cancelAutoRename(topicId)
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
items.push({
|
|
||||||
label: 'Auto Rename',
|
|
||||||
onClick: () => autoRenameTopic(topicId)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push({
|
openDropdown(e, () => {
|
||||||
label: 'Rename',
|
const topic = topics.value.find(t => t.id === topicId);
|
||||||
disabled: isRenaming ?? false,
|
const isRenaming = activeAutoRenames.has(topicId) && topic?.renaming;
|
||||||
onClick: () => startRename(topicId, topics.value?.find(t => t.id === topicId)?.name || '')
|
|
||||||
});
|
|
||||||
|
|
||||||
items.push({
|
return [
|
||||||
label: 'Delete',
|
isRenaming
|
||||||
danger: true,
|
? { label: 'Cancel Auto Rename', onClick: () => cancelAutoRename(topicId) }
|
||||||
onClick: () => deleteTopic(topicId)
|
: { label: 'Auto Rename', onClick: () => autoRenameTopic(topicId) },
|
||||||
});
|
{ label: 'Rename', disabled: isRenaming ?? false, onClick: () => startRename(topicId, topic?.name || '') },
|
||||||
|
{ label: 'Delete', danger: true, onClick: () => deleteTopic(topicId) }
|
||||||
return items;
|
];
|
||||||
};
|
}, { minWidth: '120px', placement: 'right' });
|
||||||
|
break;
|
||||||
openDropdown(e, itemsFactory, { minWidth: '120px', placement: 'right' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,14 +156,11 @@ onMounted(() => {
|
|||||||
// simply preload the topic page
|
// simply preload the topic page
|
||||||
preloadRouteComponents(`/agent/${route.params.id}/topic/42`);
|
preloadRouteComponents(`/agent/${route.params.id}/topic/42`);
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeAgents?.();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="flex flex-col gap-1">
|
<nav ref="navRef"
|
||||||
|
class="max-h-full h-full overflow-auto [scrollbar-color:#888_transparent] [scrollbar-width:thin] [scrollbar-gutter:stable]">
|
||||||
|
|
||||||
<!-- Agent Info Link -->
|
<!-- Agent Info Link -->
|
||||||
<div class="mt-2 flex flex-col">
|
<div class="mt-2 flex flex-col">
|
||||||
@@ -193,18 +175,14 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Collapsible :is-open="topicsOpen">
|
<Collapsible :is-open="topicsOpen">
|
||||||
<div @click="handleNavClick"
|
<div @click="handleNavClick" class="mt-1 flex flex-col transform-origin-center-top">
|
||||||
class="mt-1 gap-1 flex flex-col transform-origin-center-top [content-visibility:auto] [contain-intrinsic-size:0_36px]">
|
<RowVirtualizerFixed :scroll-element="navRef" key-field="id" :prerender="50" :items="topics"
|
||||||
<a v-for="topic in topics" :key="topic.id" data-action="navigate" :data-topic-id="topic.id"
|
:item-size="40" :overscan="20">
|
||||||
:href="`/agent/${route.params.id}/topic/${topic.id}`" :aria-label="topic.name" :class="[
|
<template v-slot="{ item: topic }">
|
||||||
'group relative decoration-none flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9',
|
<a :key="topic.id" data-action="navigate" :data-topic-id="topic.id"
|
||||||
'px-2',
|
:href="`/agent/${route.params.id}/topic/${topic.id}`" :aria-label="topic.name"
|
||||||
topic.id === route.params.topicId
|
class="mt-1 group px-2 decoration-none flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9 text-[var(--text-secondary)] hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] focus:text-[var(--text-primary)]"
|
||||||
? 'text-[var(--text-primary)] bg-[var(--color-hover)]'
|
:class="{ 'bg-[var(--color-hover)]': route.params.topicId === topic.id }">
|
||||||
: 'text-[var(--text-secondary)] hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)]'
|
|
||||||
]">
|
|
||||||
<div class="flex items-center gap-2 max-w-full flex-1">
|
|
||||||
<div class="flex justify-between items-center w-full">
|
|
||||||
<input v-if="renameTopicId === topic.id && !topic.renaming" id="topic-rename-input"
|
<input v-if="renameTopicId === topic.id && !topic.renaming" id="topic-rename-input"
|
||||||
v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename"
|
v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename"
|
||||||
@blur="saveRename"
|
@blur="saveRename"
|
||||||
@@ -218,18 +196,16 @@ onUnmounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div data-action="toggle-dropdown"
|
<div data-action="toggle-dropdown"
|
||||||
class="shrink-0 opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
class="text-[var(--text-secondary)] shrink-0 opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
<svg class="pointer-events-none" xmlns="http://www.w3.org/2000/svg" width="18"
|
<span class="h-4.5 w-4.5 i-tabler:dots"></span>
|
||||||
height="18" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor"
|
|
||||||
d="M7 12a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0m7 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</template>
|
||||||
</a>
|
</RowVirtualizerFixed>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- text-[var(--text-secondary)] hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] -->
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { agents, unsubscribe, createAgent } = await useAgents();
|
const { agents, createAgent } = useAgents();
|
||||||
const agentsOpen = ref(true);
|
const agentsOpen = ref(true);
|
||||||
const creatingAgent = ref(false);
|
const creatingAgent = ref(false);
|
||||||
|
|
||||||
|
const { openDropdown, dropdownState, closeDropdown } = useDropdown();
|
||||||
|
|
||||||
|
const navRef = ref<HTMLElement | null>(null);
|
||||||
const toggleAgentsList = () => {
|
const toggleAgentsList = () => {
|
||||||
agentsOpen.value = !agentsOpen.value;
|
agentsOpen.value = !agentsOpen.value;
|
||||||
};
|
};
|
||||||
@@ -11,17 +14,62 @@ const newAgent = async () => {
|
|||||||
creatingAgent.value = true;
|
creatingAgent.value = true;
|
||||||
try {
|
try {
|
||||||
const agent = await createAgent();
|
const agent = await createAgent();
|
||||||
if (agent) return navigateTo(`/agent/${agent.id}`);
|
return navigateTo(`/agent/${agent.id}`);
|
||||||
} finally {
|
} finally {
|
||||||
creatingAgent.value = false;
|
creatingAgent.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => unsubscribe?.());
|
const handleNavClick = (e: MouseEvent) => {
|
||||||
|
const trigger = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
|
||||||
|
if (!trigger) return;
|
||||||
|
|
||||||
|
const agentId = trigger.dataset.agentId || (trigger.closest('[data-agent-id]') as HTMLElement | null)?.dataset.agentId;
|
||||||
|
if (!agentId) return;
|
||||||
|
|
||||||
|
const action = trigger.dataset.action;
|
||||||
|
switch (action) {
|
||||||
|
case 'navigate':
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
return navigateTo(`/agent/${agentId}`);
|
||||||
|
|
||||||
|
case 'toggle-dropdown':
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (dropdownState.open) {
|
||||||
|
closeDropdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// openDropdown(e, () => {
|
||||||
|
// const agent = agents.value.find(a => a.id === agentId);
|
||||||
|
// if (agent) {
|
||||||
|
// const isRenaming = activeAutoRenames.has(agentId) && agent?.renaming;
|
||||||
|
|
||||||
|
// return [
|
||||||
|
// isRenaming
|
||||||
|
// ? { label: 'Cancel Auto Rename', onClick: () => cancelAutoRename(agentId) }
|
||||||
|
// : { label: 'Auto Rename', onClick: () => autoRenameAgent(agentId) },
|
||||||
|
// { label: 'Rename', disabled: isRenaming ?? false, onClick: () => startRename(agentId, agent?.name || '') },
|
||||||
|
// { label: 'Delete', danger: true, onClick: () => deleteAgent(agentId) }
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return [];
|
||||||
|
// }, { minWidth: '120px', placement: 'right' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// simply preload the topic page
|
||||||
|
preloadRouteComponents(`/agent/42`);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="flex flex-col gap-1">
|
<nav ref="navRef" class="flex flex-col gap-1 overflow-auto">
|
||||||
<SidenavItem name="Search" icon="mynaui:search" />
|
<SidenavItem name="Search" icon="mynaui:search" />
|
||||||
<SidenavItem to="/" name="Home" icon="mynaui:home" />
|
<SidenavItem to="/" name="Home" icon="mynaui:home" />
|
||||||
|
|
||||||
@@ -46,8 +94,32 @@ onUnmounted(() => unsubscribe?.());
|
|||||||
<span>New Agent</span>
|
<span>New Agent</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<SidenavItem v-for="agent in agents" :key="agent.id" :to="`/agent/${agent.id}`" :name="agent.name"
|
<div @click="handleNavClick" class="flex flex-col">
|
||||||
icon="mynaui:check-hexagon" />
|
<RowVirtualizerFixed :scroll-element="navRef" key-field="id" :items="agents" :prerender="50"
|
||||||
|
:item-size="40" :overscan="20">
|
||||||
|
<template v-slot="{ item: agent }">
|
||||||
|
<a data-action="navigate" :data-agent-id="agent.id" :href="`/agent/${agent.id}`"
|
||||||
|
:aria-label="agent.name"
|
||||||
|
class="mt-1 group px-2 decoration-none flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9 text-[var(--text-secondary)] hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] focus:text-[var(--text-primary)]">
|
||||||
|
<!-- <input v-if="renameTopicId === topic.id && !topic.renaming" id="topic-rename-input"
|
||||||
|
v-model="newTopicName" @keydown.enter="saveRename" @keydown.escape="cancelRename"
|
||||||
|
@blur="saveRename"
|
||||||
|
class="flex-1 bg-transparent border-none outline-none text-sm font-medium text-[var(--text-primary)] px-0 min-w-0" />
|
||||||
|
<div v-else-if="topic.renaming" class="flex w-full">
|
||||||
|
<Icon name="svg-spinners:3-dots-fade" class="text-6" />
|
||||||
|
</div> -->
|
||||||
|
<span class="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{{ agent.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div data-action="toggle-dropdown"
|
||||||
|
class="text-[var(--text-secondary)] shrink-0 opacity-0 group-hover:opacity-100 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] transition-opacity duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
|
||||||
|
<span class="h-4.5 w-4.5 i-tabler:dots"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</RowVirtualizerFixed>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const navKind = computed(() => {
|
|||||||
<div :style="{ minWidth: `${sidebarWidth}px` }" class="flex flex-col h-full justify-between">
|
<div :style="{ minWidth: `${sidebarWidth}px` }" class="flex flex-col h-full justify-between">
|
||||||
<div class="flex flex-col h-full max-h-full overflow-y-hidden">
|
<div class="flex flex-col h-full max-h-full overflow-y-hidden">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="relative flex flex-row gap-2 justify-between items-center mb-1.5">
|
<div class="relative flex flex-row gap-2 justify-between items-center mb-1.5 h-8">
|
||||||
|
|
||||||
<SidenavHeader v-if="navKind === 'home'" />
|
<SidenavHeader v-if="navKind === 'home'" />
|
||||||
<SidenavHeaderAgent v-else-if="navKind === 'agent'" />
|
<SidenavHeaderAgent v-else-if="navKind === 'agent'" />
|
||||||
@@ -135,11 +135,8 @@ const navKind = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Menu -->
|
<!-- Main Menu -->
|
||||||
<div
|
<SidenavNavHome v-if="navKind === 'home'" />
|
||||||
class="max-h-full h-full overflow-auto [scrollbar-color:#888_transparent] [scrollbar-width:thin] [scrollbar-gutter:stable]">
|
<SidenavNavAgent v-else-if="navKind === 'agent'" />
|
||||||
<SidenavNavHome v-if="navKind === 'home'" />
|
|
||||||
<SidenavNavAgent v-else-if="navKind === 'agent'" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between pt-2">
|
<div class="flex justify-between pt-2">
|
||||||
|
|||||||
@@ -1,26 +1,55 @@
|
|||||||
import type schema from "#triplit/schema";
|
|
||||||
import type { Entity } from "@triplit/client";
|
import type { Entity } from "@triplit/client";
|
||||||
|
import type schema from "#triplit/schema";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { assert } from "~~/utils/assert";
|
||||||
|
|
||||||
export const useAgents = async () => {
|
export type Agent = Readonly<Entity<typeof schema, 'agents'> & { topics: Readonly<Entity<typeof schema, 'topics'>>[] }>;
|
||||||
|
|
||||||
|
export const useAgents = () => {
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const { results: agents, unsubscribe } = await useQuery(
|
// dont leaking between different users/requests
|
||||||
'agents',
|
if (!nuxtApp._agentsState) {
|
||||||
triplit,
|
nuxtApp._agentsState = {
|
||||||
triplit
|
list: ref<Agent[]>([]),
|
||||||
.query('agents')
|
initPromise: null as Promise<void> | null,
|
||||||
.Include('topics', (rel) => rel('topics').Order('createdAt', 'DESC'))
|
};
|
||||||
.Order('createdAt', 'ASC')
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const createAgent = async (): Promise<Readonly<Entity<typeof schema, 'agents'>> | null> => {
|
const state = nuxtApp._agentsState as {
|
||||||
|
list: Ref<Agent[]>;
|
||||||
|
initPromise: Promise<void> | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
if (state.initPromise) return;
|
||||||
|
|
||||||
|
state.initPromise = (async () => {
|
||||||
|
const query = triplit.query('agents')
|
||||||
|
.Include('topics', (rel) => rel('topics').Order('createdAt', 'DESC'))
|
||||||
|
.Order('createdAt', 'ASC');
|
||||||
|
|
||||||
|
const { results } = await useQuery('agents', triplit, query)
|
||||||
|
watch(results, (newAgents) => {
|
||||||
|
console.log("newAgents", newAgents);
|
||||||
|
state.list.value = newAgents as unknown as Agent[] || [];
|
||||||
|
}, { immediate: true, flush: 'sync' });
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAgent = (id: MaybeRef<string>) => {
|
||||||
|
return computed(() => state.list.value.find((agent) => agent.id === id) || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAgent = async () => {
|
||||||
|
const triplit = useTriplitClient();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
if (!user.value) {
|
if (!user.value) throw new Error('No user');
|
||||||
console.error('No user');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return triplit.insert('agents', {
|
const id = nanoid();
|
||||||
|
await triplit.insert('agents', {
|
||||||
|
id,
|
||||||
name: 'New Agent',
|
name: 'New Agent',
|
||||||
userId: user.value.id,
|
userId: user.value.id,
|
||||||
systemPrompt: 'You are a helpful assistant.',
|
systemPrompt: 'You are a helpful assistant.',
|
||||||
@@ -28,14 +57,16 @@ export const useAgents = async () => {
|
|||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
|
assert('flush' in triplit);
|
||||||
|
await triplit.flush();
|
||||||
|
|
||||||
|
return state.list.value.find((agent) => agent.id === id)!;
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
agents,
|
init,
|
||||||
unsubscribe,
|
agents: state.list,
|
||||||
getAgent: (id: string) => {
|
getAgent,
|
||||||
return agents.value?.find((a: any) => a.id === id);
|
createAgent
|
||||||
},
|
|
||||||
createAgent,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,68 +1,78 @@
|
|||||||
import { schema } from '#triplit/schema';
|
// // composables/useModels.ts
|
||||||
import type { Entity } from '@triplit/client';
|
import type { Entity } from '@triplit/client';
|
||||||
|
import type schema from '#triplit/schema';
|
||||||
|
|
||||||
export type ModelWithProvider = Entity<typeof schema, 'models'> & {
|
type Provider = Entity<typeof schema, 'providers'>;
|
||||||
provider: Entity<typeof schema, 'providers'>;
|
type Model = Entity<typeof schema, 'models'>;
|
||||||
};
|
|
||||||
|
|
||||||
export type ProviderWithModels = Entity<typeof schema, 'providers'> & {
|
export interface ModelWithProvider extends Model {
|
||||||
models: Entity<typeof schema, 'models'>[];
|
provider: Provider;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useModels = async () => {
|
export interface ProviderWithModels extends Provider {
|
||||||
const nuxtApp = useNuxtApp() as any;
|
models: Model[];
|
||||||
|
}
|
||||||
|
|
||||||
if (!nuxtApp._modelsSubscription) {
|
export const useModels = () => {
|
||||||
if (!nuxtApp._modelsPromise) {
|
const nuxtApp = useNuxtApp();
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const providersQuery = triplit
|
if (!nuxtApp._modelsState) {
|
||||||
.query('providers')
|
nuxtApp._modelsState = {
|
||||||
.Include('models');
|
providers: shallowRef([]),
|
||||||
|
isReady: ref(false)
|
||||||
const start = Date.now();
|
};
|
||||||
nuxtApp._modelsPromise = useQuery('providers', triplit, providersQuery).then((sub) => {
|
|
||||||
nuxtApp._modelsSubscription = sub;
|
|
||||||
console.log("fetching providers took", Date.now() - start);
|
|
||||||
return sub;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await nuxtApp._modelsPromise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { results: providers } = nuxtApp._modelsSubscription;
|
|
||||||
|
|
||||||
if (!nuxtApp._allModels) {
|
const state = nuxtApp._modelsState as {
|
||||||
nuxtApp._allModels = computed<ModelWithProvider[]>(() => {
|
providers: Ref<ProviderWithModels[]>;
|
||||||
if (!providers.value) return [];
|
initPromise: Promise<void> | null;
|
||||||
|
};
|
||||||
|
|
||||||
const list: ModelWithProvider[] = [];
|
const init = () => {
|
||||||
|
if (state.initPromise) return;
|
||||||
|
|
||||||
for (const provider of (providers.value as ProviderWithModels[])) {
|
state.initPromise = (async () => {
|
||||||
if (!provider.enabled) continue;
|
const query = triplit.query('providers').Include('models');
|
||||||
|
|
||||||
for (const model of provider.models) {
|
const { results } = await useQuery('providers', triplit, query)
|
||||||
if (!model.enabled) continue;
|
watch(results, (newProviders) => {
|
||||||
|
state.providers.value = newProviders as unknown as ProviderWithModels[] || [];
|
||||||
|
}, { immediate: true, flush: 'sync' });
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
list.push({
|
const allModels = computed<ModelWithProvider[]>(() => {
|
||||||
...model,
|
const result: ModelWithProvider[] = [];
|
||||||
provider
|
for (const provider of state.providers.value) {
|
||||||
} as unknown as ModelWithProvider);
|
if (!provider.enabled) continue;
|
||||||
}
|
for (const model of provider.models || []) {
|
||||||
|
if (!model.enabled) continue;
|
||||||
|
result.push({ ...model, provider });
|
||||||
}
|
}
|
||||||
return list;
|
}
|
||||||
});
|
return result;
|
||||||
}
|
});
|
||||||
|
|
||||||
const getFirstAvailableModel = (): ModelWithProvider | null => {
|
const getModel = (id: string): ModelWithProvider | undefined => {
|
||||||
if (nuxtApp._allModels.value.length === 0) return null;
|
return allModels.value.find((model) => model.id === id);
|
||||||
return nuxtApp._allModels.value[0]!;
|
};
|
||||||
|
|
||||||
|
const getProvider = (id: string): ProviderWithModels | undefined => {
|
||||||
|
return state.providers.value.find((provider) => provider.id === id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstAvailableModel = () => {
|
||||||
|
return allModels.value[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providers: providers as Ref<ProviderWithModels[]>,
|
init,
|
||||||
allModels: nuxtApp._allModels as ComputedRef<ModelWithProvider[]>,
|
providers: state.providers,
|
||||||
getFirstAvailableModel,
|
allModels,
|
||||||
unsubscribe: () => { },
|
getModel,
|
||||||
|
getProvider,
|
||||||
|
getFirstAvailableModel
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
preloadRouteComponents('/');
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full grid place-items-center">
|
<div class="h-full w-full grid place-items-center">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ useKeyboardShortcuts();
|
|||||||
<Icon class="text-5" name="mynaui:panel-left-open" />
|
<Icon class="text-5" name="mynaui:panel-left-open" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div class="flex items-center gap-2">
|
|
||||||
<div id="primary-loader-target"></div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -8,16 +8,10 @@ const inputValue = ref('');
|
|||||||
const pendingMessage = ref<Message | null>(null);
|
const pendingMessage = ref<Message | null>(null);
|
||||||
|
|
||||||
const { createTopic, sendMessage, autoRename } = useChat(route.params.id as string);
|
const { createTopic, sendMessage, autoRename } = useChat(route.params.id as string);
|
||||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
const { getAgent } = useAgents();
|
||||||
const { providers, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers } = useModels();
|
||||||
|
|
||||||
const agent = computed(() => {
|
const agent = getAgent(route.params.id as string);
|
||||||
if (route.params.id === null || typeof route.params.id !== 'string') {
|
|
||||||
throw new Error('Invalid agent ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAgent(route.params.id)!;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!agent.value) navigateTo('/');
|
if (!agent.value) navigateTo('/');
|
||||||
|
|
||||||
@@ -85,11 +79,6 @@ const handleSubmit = async (message: string, model: ModelWithProvider | null) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeAgents?.();
|
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
const { getAgent } = useAgents();
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const agent = computed(() => {
|
const agent = getAgent(route.params.id as string);
|
||||||
if (route.params.id === null || typeof route.params.id !== 'string') {
|
|
||||||
throw new Error('Invalid agent ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAgent(route.params.id)!;
|
if (agent.value === undefined) navigateTo('/');
|
||||||
});
|
|
||||||
|
|
||||||
// if (agent.value === undefined) navigateTo('/');
|
|
||||||
|
|
||||||
const handleInput = async (e: Event) => {
|
const handleInput = async (e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
@@ -20,7 +14,7 @@ const handleInput = async (e: Event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await triplit.update('agents', agent.value.id, { name: target.value });
|
await triplit.update('agents', agent.value!.id, { name: target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeSystemPrompt = async (e: Event) => {
|
const changeSystemPrompt = async (e: Event) => {
|
||||||
@@ -31,14 +25,10 @@ const changeSystemPrompt = async (e: Event) => {
|
|||||||
value = undefined;
|
value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
await triplit.update('agents', agent.value.id, {
|
await triplit.update('agents', agent.value!.id, {
|
||||||
systemPrompt: target.value,
|
systemPrompt: target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
unsubscribeAgents?.();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -10,16 +10,10 @@ const chatPaneWrapper = ref<HTMLElement | null>(null);
|
|||||||
const inputValue = ref('');
|
const inputValue = ref('');
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { sendMessage, regenerateMessage } = useChat(route.params.id as string);
|
const { sendMessage, regenerateMessage } = useChat(route.params.id as string);
|
||||||
const { getAgent, unsubscribe: unsubscribeAgents } = await useAgents();
|
const { getAgent } = useAgents();
|
||||||
const { providers, allModels, unsubscribe: unsubscribeModels } = await useModels();
|
const { providers, allModels } = useModels();
|
||||||
|
|
||||||
const agent = computed(() => {
|
const agent = getAgent(route.params.id as string);
|
||||||
if (route.params.id === null || typeof route.params.id !== 'string') {
|
|
||||||
throw new Error('Invalid agent ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAgent(route.params.id)!;
|
|
||||||
});
|
|
||||||
|
|
||||||
const topicQuery = computed(() =>
|
const topicQuery = computed(() =>
|
||||||
triplit
|
triplit
|
||||||
@@ -273,8 +267,6 @@ onUnmounted(() => {
|
|||||||
unsubscribeMessages?.();
|
unsubscribeMessages?.();
|
||||||
unsubscribeParts?.();
|
unsubscribeParts?.();
|
||||||
unsubscribeGenerations?.();
|
unsubscribeGenerations?.();
|
||||||
unsubscribeAgents?.();
|
|
||||||
unsubscribeModels?.();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+5
-8
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type schema from '#triplit/schema';
|
|
||||||
import type { Entity } from '@triplit/client';
|
|
||||||
import { assert } from '~~/utils/assert';
|
import { assert } from '~~/utils/assert';
|
||||||
|
import type { Agent } from '~/composables/useAgents';
|
||||||
|
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
|
||||||
const { agents, unsubscribe: unsubscribeAgents, createAgent } = await useAgents();
|
const { agents, createAgent } = useAgents();
|
||||||
const { providers, unsubscribe: unsubscribeModels, getFirstAvailableModel, allModels } = await useModels();
|
const { providers, getFirstAvailableModel, allModels } = useModels();
|
||||||
|
|
||||||
const taglines = {
|
const taglines = {
|
||||||
morning: [
|
morning: [
|
||||||
@@ -74,12 +73,12 @@ const typeWriter = (time: 'morning' | 'afternoon' | 'evening') => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const agent = computed(() => {
|
const agent = computed(() => {
|
||||||
return agents.value?.[0];
|
return agents.value?.[0] ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChatSubmit = async (message: string, model: ModelWithProvider | null) => {
|
const handleChatSubmit = async (message: string, model: ModelWithProvider | null) => {
|
||||||
console.log('Message submitted:', message, agents);
|
console.log('Message submitted:', message, agents);
|
||||||
let agent: Entity<typeof schema, 'agents'> | null = agents.value?.[0] ?? null;
|
let agent: Agent | null = agents.value?.[0] ?? null;
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
assert('flush' in triplit);
|
assert('flush' in triplit);
|
||||||
@@ -162,8 +161,6 @@ onUnmounted(() => {
|
|||||||
if (typeWriterInterval !== null) {
|
if (typeWriterInterval !== null) {
|
||||||
clearTimeout(typeWriterInterval);
|
clearTimeout(typeWriterInterval);
|
||||||
}
|
}
|
||||||
unsubscribeAgents?.();
|
|
||||||
unsubscribeModels?.();
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
|
|
||||||
|
// Warm up the caches
|
||||||
|
await Promise.all([
|
||||||
|
useAgents().init(),
|
||||||
|
useModels().init()
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Global data ready');
|
||||||
|
});
|
||||||
+30
-8
@@ -10,6 +10,7 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ name: 'format-detection', content: 'telephone=no' },
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: 'A chat-based AI assistant for your LLMs.',
|
content: 'A chat-based AI assistant for your LLMs.',
|
||||||
@@ -81,6 +82,12 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
vue: {
|
||||||
|
config: {
|
||||||
|
performance: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
@@ -109,10 +116,10 @@ export default defineNuxtConfig({
|
|||||||
'@vue/devtools-kit',
|
'@vue/devtools-kit',
|
||||||
'@sentry/nuxt',
|
'@sentry/nuxt',
|
||||||
'@nuxt/hints/runtime/hydration/component',
|
'@nuxt/hints/runtime/hydration/component',
|
||||||
'vue-virtual-scroller',
|
'@nuxt/hints/runtime/lazy-load/composables',
|
||||||
|
'@tanstack/vue-virtual',
|
||||||
'shiki',
|
'shiki',
|
||||||
'better-auth/vue',
|
'better-auth/vue',
|
||||||
'big.js',
|
|
||||||
'unified',
|
'unified',
|
||||||
'remark-gfm',
|
'remark-gfm',
|
||||||
'remark-parse',
|
'remark-parse',
|
||||||
@@ -122,9 +129,17 @@ export default defineNuxtConfig({
|
|||||||
'unist-util-visit',
|
'unist-util-visit',
|
||||||
'@triplit/client',
|
'@triplit/client',
|
||||||
'@triplit/db',
|
'@triplit/db',
|
||||||
'comlink'
|
'sorted-btree',
|
||||||
|
'elen',
|
||||||
|
'comlink',
|
||||||
|
'nanoid'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
commonjsOptions: {
|
||||||
|
transformMixedEsModules: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
nitro: {
|
nitro: {
|
||||||
@@ -138,7 +153,14 @@ export default defineNuxtConfig({
|
|||||||
// viewTransition: true,
|
// viewTransition: true,
|
||||||
// },
|
// },
|
||||||
|
|
||||||
modules: ['@vue-macros/nuxt', '@nuxt/hints', '@nuxt/icon', '@unocss/nuxt', 'triplit-nuxt', '@sentry/nuxt/module', '@nuxt/fonts'],
|
modules: [
|
||||||
|
'@nuxt/hints',
|
||||||
|
'@nuxt/icon',
|
||||||
|
'@unocss/nuxt',
|
||||||
|
'triplit-nuxt',
|
||||||
|
// '@sentry/nuxt/module',
|
||||||
|
'@nuxt/fonts'
|
||||||
|
],
|
||||||
|
|
||||||
triplit: {
|
triplit: {
|
||||||
schema_path: './triplit/schema.ts',
|
schema_path: './triplit/schema.ts',
|
||||||
@@ -159,10 +181,10 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
|
|
||||||
sentry: {
|
// sentry: {
|
||||||
org: 'veridian-ol',
|
// org: 'veridian-ol',
|
||||||
project: 'javascript-nuxt',
|
// project: 'javascript-nuxt',
|
||||||
},
|
// },
|
||||||
|
|
||||||
sourcemap: {
|
sourcemap: {
|
||||||
client: 'hidden',
|
client: 'hidden',
|
||||||
|
|||||||
+7
-8
@@ -8,8 +8,7 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare",
|
"postinstall": "nuxt prepare"
|
||||||
"knip": "knip"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/cerebras": "^2.0.34",
|
"@ai-sdk/cerebras": "^2.0.34",
|
||||||
@@ -18,15 +17,15 @@
|
|||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
"@daveyplate/better-auth-triplit": "^0.2.2",
|
"@daveyplate/better-auth-triplit": "^0.2.2",
|
||||||
"@iconify-json/mynaui": "^1.2.17",
|
"@iconify-json/mynaui": "^1.2.17",
|
||||||
"@nuxt/fonts": "0.13.0",
|
"@nuxt/fonts": "0.14.0",
|
||||||
"@nuxt/hints": "1.0.0-alpha.5",
|
"@nuxt/hints": "1.0.0-alpha.5",
|
||||||
"@nuxt/icon": "2.2.0",
|
"@nuxt/icon": "2.2.0",
|
||||||
"@openrouter/ai-sdk-provider": "^2.2.3",
|
"@openrouter/ai-sdk-provider": "^2.2.3",
|
||||||
"@sentry/nuxt": "^10.39.0",
|
"@sentry/nuxt": "^10.39.0",
|
||||||
|
"@tanstack/vue-virtual": "^3.13.18",
|
||||||
"@triplit/client": "^1.0.50",
|
"@triplit/client": "^1.0.50",
|
||||||
"@triplit/server": "^1.1.8",
|
"@triplit/server": "^1.1.8",
|
||||||
"@types/big.js": "^6.2.2",
|
"@types/big.js": "^6.2.2",
|
||||||
"@vue-macros/nuxt": "^3.1.2",
|
|
||||||
"ai": "^6.0.89",
|
"ai": "^6.0.89",
|
||||||
"ai-sdk-ollama": "^3.7.1",
|
"ai-sdk-ollama": "^3.7.1",
|
||||||
"better-auth": "^1.4.18",
|
"better-auth": "^1.4.18",
|
||||||
@@ -34,7 +33,7 @@
|
|||||||
"comlink": "^4.4.2",
|
"comlink": "^4.4.2",
|
||||||
"glob": "^13.0.5",
|
"glob": "^13.0.5",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"nuxt": "4.2.2",
|
"nuxt": "^4.3.1",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
@@ -44,8 +43,7 @@
|
|||||||
"triplit-nuxt": "0.3.1-prerelease.5",
|
"triplit-nuxt": "0.3.1-prerelease.5",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"vue": "^3.5.28",
|
"vue": "^3.5.28",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^5.0.3",
|
||||||
"vue-virtual-scroller": "^2.0.0-beta.8",
|
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -63,6 +61,7 @@
|
|||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
"@sentry/cli",
|
||||||
"core-js",
|
"core-js",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"unrs-resolver"
|
"unrs-resolver"
|
||||||
@@ -73,6 +72,6 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@vercel/nft": "^0.27.4",
|
"@vercel/nft": "^0.27.4",
|
||||||
"vite": "8.0.0-beta.0"
|
"vite": "8.0.0-beta.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,24 +90,6 @@ index 09fcba1b0df49971de59b5d9f11171e5ea88aee7..0ce27b58838db11fd51fcf873a182d52
|
|||||||
db = new DB({ ...options, schema: savedSchema });
|
db = new DB({ ...options, schema: savedSchema });
|
||||||
let schemaChange = undefined;
|
let schemaChange = undefined;
|
||||||
// A schema is provided, attempt to apply it
|
// A schema is provided, attempt to apply it
|
||||||
diff --git a/dist/index.js b/dist/index.js
|
|
||||||
index 5155e43bb742663d121d2c5a50275398d8b79b36..9fbd6beac969f25612d74a57b845b973ea914637 100644
|
|
||||||
--- a/dist/index.js
|
|
||||||
+++ b/dist/index.js
|
|
||||||
@@ -1,4 +1,3 @@
|
|
||||||
-import './polyfills.js';
|
|
||||||
export * from './codec.js';
|
|
||||||
export * from './db.js';
|
|
||||||
export * from './db-transaction.js';
|
|
||||||
diff --git a/dist/polyfills.d.ts b/home/zoeissleeping/.cache/.bun/install/cache/@triplit/db@1.1.10@@@1/dist/polyfills.d.ts
|
|
||||||
deleted file mode 100644
|
|
||||||
index 825f04fd5dba36fd87c308e87e15b2c3c5be8712..0000000000000000000000000000000000000000
|
|
||||||
diff --git a/dist/polyfills.js b/home/zoeissleeping/.cache/.bun/install/cache/@triplit/db@1.1.10@@@1/dist/polyfills.js
|
|
||||||
deleted file mode 100644
|
|
||||||
index 264a2151aaf63c967a2f8820454d2692f4522c99..0000000000000000000000000000000000000000
|
|
||||||
diff --git a/dist/polyfills.js.map b/home/zoeissleeping/.cache/.bun/install/cache/@triplit/db@1.1.10@@@1/dist/polyfills.js.map
|
|
||||||
deleted file mode 100644
|
|
||||||
index 23685d5015568d42f4273c442fff1dacac3151d4..0000000000000000000000000000000000000000
|
|
||||||
diff --git a/dist/schema/data-types/type.js b/dist/schema/data-types/type.js
|
diff --git a/dist/schema/data-types/type.js b/dist/schema/data-types/type.js
|
||||||
index 2e1281c5577b48ffe609a08da933b5f342a485fa..e88487852f6c49185144a9057ffb11d93a3b6cc8 100644
|
index 2e1281c5577b48ffe609a08da933b5f342a485fa..e88487852f6c49185144a9057ffb11d93a3b6cc8 100644
|
||||||
--- a/dist/schema/data-types/type.js
|
--- a/dist/schema/data-types/type.js
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type Entity } from '@triplit/client';
|
import { type Entity } from '@triplit/client';
|
||||||
import Big from 'big.js';
|
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { SupportedModalities } from '~/types/model';
|
import { SupportedModalities } from '~/types/model';
|
||||||
import { Providers } from '~/types/model';
|
import { Providers } from '~/types/model';
|
||||||
@@ -246,14 +245,65 @@ const getModelData = (modelId: string, providerId: string, modelsDevData: any) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBig = (bigValue: Big) => {
|
// const formatBig = (bigValue: Big) => {
|
||||||
let str = bigValue.toString();
|
// let str = bigValue.toString();
|
||||||
|
|
||||||
if (!str.includes('.')) return str + '.00';
|
// if (!str.includes('.')) return str + '.00';
|
||||||
if (str.split('.')[1]!.length === 1) return str + '0';
|
// if (str.split('.')[1]!.length === 1) return str + '0';
|
||||||
|
|
||||||
return str;
|
// return str;
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
// this function multiplies a string in the format of 'D.DD' by 1_000_000
|
||||||
|
// it does this by finding the first digit that is not a zero, and then
|
||||||
|
// left shifting it in decimal by 3 places
|
||||||
|
const lshDecimal = (number: string, shift: number) => {
|
||||||
|
if (number.length === 0) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = '';
|
||||||
|
let isNegative = number[0] === '-';
|
||||||
|
let [integerPart, fractionalPart] = number.substring(isNegative ? 1 : 0).split('.');
|
||||||
|
|
||||||
|
if (!fractionalPart) {
|
||||||
|
fractionalPart = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shift <= fractionalPart.length) {
|
||||||
|
integerPart += fractionalPart.substring(0, shift);
|
||||||
|
fractionalPart = fractionalPart.substring(shift);
|
||||||
|
} else if (shift > fractionalPart.length) {
|
||||||
|
integerPart += fractionalPart;
|
||||||
|
for (let i = 0; i < shift - fractionalPart.length; i++) {
|
||||||
|
integerPart += '0';
|
||||||
|
}
|
||||||
|
fractionalPart = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNegative) {
|
||||||
|
value += '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
integerPart = integerPart!.replace(/^0+/, '');
|
||||||
|
if (integerPart.length === 0) {
|
||||||
|
integerPart = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
value += integerPart;
|
||||||
|
|
||||||
|
if (fractionalPart.length > 0) {
|
||||||
|
if (fractionalPart.length < 2) {
|
||||||
|
fractionalPart += '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
value += '.' + fractionalPart;
|
||||||
|
} else {
|
||||||
|
value += '.00';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeResponse = async (response: Record<string, any>, provider: typeof Providers[number], baseUrl: string, modelsDevData: any) => {
|
const normalizeResponse = async (response: Record<string, any>, provider: typeof Providers[number], baseUrl: string, modelsDevData: any) => {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
@@ -289,35 +339,37 @@ const normalizeResponse = async (response: Record<string, any>, provider: typeof
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case 'prompt':
|
case 'prompt':
|
||||||
case 'completion':
|
case 'completion':
|
||||||
case 'request':
|
|
||||||
case 'image':
|
case 'image':
|
||||||
case 'audio':
|
case 'audio':
|
||||||
case 'discount':
|
case 'discount':
|
||||||
pricing[key] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing[key] = lshDecimal(model.pricing[key], 6);
|
||||||
|
break;
|
||||||
|
case 'request':
|
||||||
|
pricing['request'] = lshDecimal(model.pricing[key], 3);
|
||||||
break;
|
break;
|
||||||
case 'image_tokens':
|
case 'image_tokens':
|
||||||
pricing['imageTokens'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['imageTokens'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'image_output':
|
case 'image_output':
|
||||||
pricing['imageOutput'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['imageOutput'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'audio_output':
|
case 'audio_output':
|
||||||
pricing['audioOutput'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['audioOutput'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'input_audio_cache':
|
case 'input_audio_cache':
|
||||||
pricing['inputAudioCache'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['inputAudioCache'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'web_search':
|
case 'web_search':
|
||||||
pricing['webSearch'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['webSearch'] = lshDecimal(model.pricing[key], 3);
|
||||||
break;
|
break;
|
||||||
case 'internal_reasoning':
|
case 'internal_reasoning':
|
||||||
pricing['internalReasoning'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['internalReasoning'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'input_cache_read':
|
case 'input_cache_read':
|
||||||
pricing['inputCacheRead'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['inputCacheRead'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
case 'input_cache_write':
|
case 'input_cache_write':
|
||||||
pricing['inputCacheWrite'] = formatBig(Big(model.pricing[key]).mul(1_000_000));
|
pricing['inputCacheWrite'] = lshDecimal(model.pricing[key], 6);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user