streaming, markdown, model selecting, and lots more

This commit is contained in:
Zoe
2026-02-02 23:16:06 -06:00
parent 8c28946703
commit d5a5945c03
114 changed files with 6109 additions and 2938 deletions
+31 -15
View File
@@ -1,45 +1,61 @@
<script setup lang="ts">
import type { DropdownItem } from '~/types/dropdown'
import type { DropdownItem } from '~/types/dropdown';
import { authClient } from '~~/lib/auth-client';
const { user, signOut } = useAuth()
const { toggle: toggleSettings } = useSettings()
const triplit = useTriplitClient();
const { user } = await useAuth();
const hovering = defineModel<boolean>({ required: true })
const { toggle: toggleSettings } = useSettings();
const profileOpen = ref(false)
const hovering = defineModel<boolean>({ required: true });
const profileOpen = ref(false);
const handleLogout = async () => {
await signOut()
}
await authClient.signOut();
if ('endSession' in triplit) {
await triplit.endSession();
}
clearNuxtData();
await navigateTo('/auth/login');
};
const profileItems: DropdownItem[] = [
{ label: 'Settings', icon: 'mynaui:cog-four', onClick: toggleSettings },
{ label: 'Log out', icon: 'mynaui:logout', divider: true, onClick: handleLogout },
]
{
label: 'Log out',
icon: 'mynaui:logout',
divider: true,
onClick: handleLogout,
},
];
</script>
<template>
<header class="flex items-center justify-between overflow-hidden">
<Dropdown v-model="profileOpen" :items="profileItems" placement="left" verticality="descending" width="100%">
<Dropdown class="overflow-hidden" v-model="profileOpen" :items="profileItems" placement="left"
verticality="descending" width="100%">
<template #trigger="{ toggle }">
<div role="button" aria-label="open user dropdown"
class="flex items-center gap-1.5 pr-2 rounded-xl hover:bg-[var(--color-highlight)] cursor-pointer transition-colors max-w-full"
<button aria-label="open user dropdown"
class="flex items-center gap-1.5 pr-2 rounded-xl hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] cursor-pointer transition-colors max-w-full"
@click="toggle">
<div
:class="['w-[28px] h-[28px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--color-neutral)] flex items-center justify-center', user?.image ? '' : 'border border-[var(--color-highlight-high)]']">
<img v-if="user?.image" :src="user.image" class="w-full h-full object-cover" />
<Icon v-else name="mynaui:user" class="w-4 h-4 text-[var(--color-subtle)]" />
<Icon v-else name="mynaui:user" class="w-4 h-4 text-[var(--color-muted)]" />
</div>
<span
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] whitespace-nowrap">{{
user!.name
}}</span>
<div :class="['flex-shrink-0 w-4 h-4 text-[var(--color-subtle)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] overflow-hidden transform-origin-center-left',
<div :class="['flex-shrink-0 w-4 h-4 text-[var(--color-muted)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)] overflow-hidden transform-origin-center-left',
hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-x-0 scale-y-90'
]">
<Icon class="text-4" name="mynaui:chevron-down" />
</div>
</div>
</button>
</template>
</Dropdown>
</header>
+45 -28
View File
@@ -1,36 +1,53 @@
<script setup lang="ts">
import type { DropdownItem } from '~/types/dropdown'
import type { DropdownItem } from '~/types/dropdown';
const { user } = useAuth()
const { agents, activeAgent } = await useAgents()
const homeButtonRef = ref<HTMLElement | null>(null)
const route = useRoute();
const { agents, getAgent } = await useAgents();
const homeButtonRef = ref<HTMLElement | null>(null);
const hovering = defineModel<boolean>({ required: true })
const initialized = ref(false)
const activeAgent = computed(() => getAgent(route.params.id as string));
const hovering = defineModel<boolean>({ required: true });
const initialized = ref(false);
let lastHovering: boolean | null = null;
onMounted(() => {
if (hovering.value) {
const width = homeButtonRef.value!.scrollWidth
homeButtonRef.value!.style.width = `calc(${width}px + 0.5rem)`
console.log(hovering.value);
if (hovering.value && homeButtonRef.value) {
const width = homeButtonRef.value.scrollWidth;
homeButtonRef.value.style.width = `calc(${width}px + 0.5rem)`;
}
watch(hovering, (value) => {
if (!initialized.value) {
initialized.value = true
if (lastHovering === value) {
console.warn('Hovering value did not change, but watcher was triggered');
}
console.log(value, lastHovering);
lastHovering = value;
if (!initialized.value) {
initialized.value = true;
}
if (!homeButtonRef.value) return;
if (value) {
const width = homeButtonRef.value!.scrollWidth
homeButtonRef.value!.style.width = `calc(${width}px + 0.5rem)`
const width = homeButtonRef.value.scrollWidth;
homeButtonRef.value.style.width = `calc(${width}px + 0.5rem)`;
} else {
homeButtonRef.value!.style.width = '0'
homeButtonRef.value.style.width = '0';
}
})
})
});
});
const agentDropdownOpen = ref(false)
onUnmounted(() => {
console.log('unmounted');
});
const agentItems: DropdownItem[] = []
const agentDropdownOpen = ref(false);
const agentItems: DropdownItem[] = [];
</script>
<template>
@@ -38,14 +55,16 @@ const agentItems: DropdownItem[] = []
<div ref="homeButtonRef" style="width: 0;"
:class="['flex flex-shrink-0 items-center overflow-hidden', initialized ? 'transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]' : '', hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']">
<NuxtLink to="/"
class="flex hover:bg-[var(--color-highlight)] rounded-lg decoration-none transition-inherit text-[var(--color-subtle)] p-1.5">
class="flex hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] rounded-lg decoration-none transition-inherit text-[var(--color-muted)] p-1.5">
<Icon name="mynaui:chevron-left" class="w-4.5 h-4.5" />
</NuxtLink>
</div>
<Dropdown v-model="agentDropdownOpen" :items="agentItems" placement="center" width="calc(80% - 1rem)">
<Dropdown class="overflow-hidden" v-model="agentDropdownOpen" :items="agentItems" placement="center"
width="calc(80% - 1rem)">
<template #trigger="{ toggle }">
<div class="flex overflow-hidden gap-1.5 pr-2 items-center cursor-pointer hover:bg-[var(--color-highlight)] rounded-lg"
<button
class="flex max-w-full gap-1.5 pr-2 items-center cursor-pointer hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] rounded-lg"
@click="toggle">
<div
:class="['w-[28px] h-[28px] flex-shrink-0 rounded-lg overflow-hidden bg-[var(--color-neutral)] flex items-center justify-center', activeAgent?.imageUrl ? '' : 'border border-[var(--color-highlight-high)]']">
@@ -57,20 +76,18 @@ const agentItems: DropdownItem[] = []
class="text-sm font-medium text-ellipsis overflow-hidden text-[var(--color-text)] whitespace-nowrap">
{{ activeAgent?.name }}
</span>
<div class="w-4 h-4 text-[var(--color-subtle)]">
<div class="w-4 h-4 text-[var(--color-muted)]">
<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>
</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">
<NuxtLink v-for="agent in agents" :to="`/agent/${agent.id}`" :key="agent.id" :class="['decoration-none whitespace-nowrap', activeAgent?.id === agent.id ? 'text-[var(--color-text)]' :
'text-[var(--color-subtle)]']" @click="agentDropdownOpen = false">
<SidenavItem :name="agent.name" icon="mynaui:check-hexagon"
:active="activeAgent?.id === agent.id" />
</NuxtLink>
<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>
+40 -10
View File
@@ -1,17 +1,47 @@
<script setup lang="ts">
const props = defineProps<{
name: string,
icon: string,
active?: boolean
}>()
name: string;
icon?: string;
to?: string;
active?: boolean;
}>();
</script>
<template>
<div role="button"
:class="['flex items-center gap-2 px-1 rounded-lg hover:bg-[var(--color-highlight)] transition-colors cursor-pointer h-9 overflow-hidden', props.active ? 'bg-[var(--color-highlight)]' : '']">
<div class="h-7 w-7 flex items-center justify-center">
<Icon class="text-4.5" :name="props.icon" />
<NuxtLink v-if="props.to" v-bind="$attrs" :to="props.to" :aria-label="props.name" :class="[
'decoration-none text-[var(--color-muted)] flex justify-between items-center shrink-0 rounded-lg transition-colors cursor-pointer h-9',
props.icon ? 'px-1' : 'px-2',
props.active
? 'text-[var(--color-text)] bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)] focus-visible:bg-[var(--color-highlight-high)]'
: 'hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)]'
]">
<div class="flex items-center gap-2 max-w-full flex-1">
<div v-if="props.icon" class="h-7 w-7 flex items-center justify-center">
<Icon class="text-4.5" :name="props.icon" />
</div>
<div class="flex justify-between items-center w-full">
<span class="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">{{ props.name
}}</span>
<slot />
</div>
</div>
<span class="text-sm font-medium overflow-hidden text-ellipsis">{{ props.name }}</span>
</div>
</NuxtLink>
<button v-else v-bind="$attrs" :aria-label="props.name" :class="[
'flex justify-between items-center shrink-0 px-1 rounded-lg transition-colors cursor-pointer h-9',
props.icon ? 'px-1' : 'px-2',
props.active
? 'text-[var(--color-text)] bg-[var(--color-highlight)] hover:bg-[var(--color-highlight-high)] focus-visible:bg-[var(--color-highlight-high)]'
: 'hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)]'
]">
<div class="flex items-center gap-2 max-w-full flex-1">
<div v-if="props.icon" class="h-7 w-7 flex items-center justify-center">
<Icon class="text-4.5" :name="props.icon" />
</div>
<div class="flex justify-between items-center w-full">
<span class="text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap">{{ props.name
}}</span>
<slot />
</div>
</div>
</button>
</template>
+128 -7
View File
@@ -1,5 +1,23 @@
<script setup lang="ts">
const route = useRoute();
const topicsListRef = ref<HTMLElement | null>(null);
const topicsListHeight = ref('auto');
const topicsListOpacity = ref(1);
const topicsListScale = ref(1);
const { getAgent } = await useAgents();
const triplit = useTriplitClient();
const activeAgent = computed(() => getAgent(route.params.id as string));
const topics = computed(() => {
if (activeAgent.value === undefined) return [];
// return the todos but sorted and in a new array do not add messages or anything to the object, JUST SORT IT
return activeAgent.value.topics
.map((topic) => topic)
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
.reverse();
});
const routeParts = computed(() => {
return route.path.replace('/agent/', '').split('/');
@@ -18,19 +36,122 @@ const pageInfo = computed(() => {
return 'agent-profile';
}
});
const topicsOpen = ref(true);
function easeInOutQuad(x: number): number {
return x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2;
}
const toggleAgentsList = () => {
if (!topicsListRef.value) return;
const animationLength = 200;
let animationStart: number | null = null;
let startHeight: number;
const startOpacity = topicsListOpacity.value;
const startScale = topicsListScale.value;
if (topicsListHeight.value === 'auto') {
startHeight = topicsListRef.value.clientHeight;
} else {
startHeight = Number(topicsListHeight.value.replace('px', ''));
}
const targetHeight = topicsOpen.value ? 0 : topicsListRef.value.scrollHeight;
const targetOpacity = topicsOpen.value ? 0 : 1;
const targetScale = topicsOpen.value ? 0.95 : 1;
topicsOpen.value = !topicsOpen.value;
const animate = (timestamp: number) => {
if (!animationStart) animationStart = timestamp;
const elapsed = timestamp - animationStart;
const progress = Math.min(elapsed / animationLength, 1);
const currentHeight = startHeight + (targetHeight - startHeight) * easeInOutQuad(progress);
const currentOpacity = startOpacity + (targetOpacity - startOpacity) * easeInOutQuad(progress);
const currentScale = startScale + (targetScale - startScale) * easeInOutQuad(progress);
topicsListOpacity.value = currentOpacity;
topicsListScale.value = currentScale;
topicsListHeight.value = `${currentHeight}px`;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
if (topicsOpen.value) {
topicsListHeight.value = 'auto';
}
}
};
requestAnimationFrame(animate);
};
const renameTopic = (topicId: string) => {
console.log('renameTopic', topicId);
};
const deleteTopic = async (topicId: string) => {
if (route.params.topicId === topicId) {
if (route.params.id) {
await navigateTo(`/agent/${route.params.id}`);
} else {
await navigateTo('/');
}
}
await triplit.delete('topics', topicId);
};
</script>
<template>
<nav class="flex flex-col gap-1">
<!-- Agent Info Link -->
<div class="mt-2">
<NuxtLink :to="`/agent/${routeParts[0]}/profile`" class="decoration-none text-[var(--color-subtle)]">
<SidenavItem name="Agent Info" icon="mynaui:info-square" :active="pageInfo === 'agent-profile'" />
</NuxtLink>
</div>
<div class="mt-2 flex flex-col">
<SidenavItem :to="`/agent/${routeParts[0]}/profile`" name="Agent Info" icon="mynaui:info-square"
:active="pageInfo === 'agent-profile'" />
<!-- Topics Section -->
<SidenavNavAgentTopics />
<!-- Topics Section -->
<button @click="toggleAgentsList"
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-colors w-full text-left">
<span class="text-sm font-medium">Topics</span>
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', topicsOpen ? '' : '-rotate-90']" />
</button>
<div ref="topicsListRef" :inert="!topicsOpen"
:style="{ height: topicsListHeight, opacity: topicsListOpacity, transform: `scale(${topicsListScale})` }"
class="mt-1 gap-1 flex flex-col transform-origin-center-top overflow-y-hidden">
<SidenavItem draggable="false" class="[&>div>div>div>[dots]]:hover:opacity-100 relative"
v-if="activeAgent?.topics !== undefined" v-for="topic in topics"
:to="`/agent/${activeAgent.id}/topic/${topic.id}`" :active="topic.id === route.params.topicId"
:name="topic.name" :key="topic.id">
<Dropdown class="shrink-0" verticality="descending" placement="right">
<template #trigger="{ toggle, isOpen }">
<div dots @click.prevent.stop="toggle"
class="opacity-0 p-1 flex items-center justify-center rounded-md hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
viewBox="0 0 24 24"><!-- Icon from Solar by 480 Design - https://creativecommons.org/licenses/by/4.0/ -->
<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>
</template>
<template #content>
<div class="shadow-lg rounded p-1 flex flex-col min-w-[120px] gap-1">
<button @click.prevent="renameTopic(topic.id)"
class="text-left px-3 py-1.5 text-sm hover:bg-[var(--color-highlight)] rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
Rename
</button>
<button @click.prevent="deleteTopic(topic.id)"
class="text-left px-3 py-1.5 text-sm text-red-600 hover:bg-red-600/20 rounded-lg transition-colors duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]">
Delete
</button>
</div>
</template>
</Dropdown>
</SidenavItem>
</div>
</div>
</nav>
</template>
-106
View File
@@ -1,106 +0,0 @@
<script setup lang="ts">
const route = useRoute();
const appState = useAppState();
const topicsListRef = ref<HTMLElement | null>(null);
const topicsListHeight = ref('auto');
const topicsListOpacity = ref(1);
const topicsListScale = ref(1);
const { topicsForActiveAgent, createTopic } = await useTopics();
const { activeAgent } = await useAgents();
const creatingTopic = ref(false);
const topicsOpen = ref(true);
function easeInOutQuad(x: number): number {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
}
const toggleAgentsList = () => {
if (!topicsListRef.value) return;
let animationLength = 200;
let animationStart: number | null = null;
let startHeight: number;
let startOpacity = topicsListOpacity.value;
let startScale = topicsListScale.value;
if (topicsListHeight.value === 'auto') {
startHeight = topicsListRef.value.clientHeight;
} else {
startHeight = Number(topicsListHeight.value.replace('px', ''));
}
let targetHeight = topicsOpen.value ? 0 : topicsListRef.value.scrollHeight;
let targetOpacity = topicsOpen.value ? 0 : 1;
let targetScale = topicsOpen.value ? 0.95 : 1;
topicsOpen.value = !topicsOpen.value;
const animate = (timestamp: number) => {
if (!animationStart) animationStart = timestamp;
const elapsed = timestamp - animationStart;
const progress = Math.min(elapsed / animationLength, 1);
const currentHeight = startHeight + (targetHeight - startHeight) * easeInOutQuad(progress);
const currentOpacity = startOpacity + (targetOpacity - startOpacity) * easeInOutQuad(progress);
const currentScale = startScale + (targetScale - startScale) * easeInOutQuad(progress);
topicsListOpacity.value = currentOpacity;
topicsListScale.value = currentScale;
topicsListHeight.value = `${currentHeight}px`;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
if (topicsOpen.value) {
topicsListHeight.value = 'auto';
}
}
};
requestAnimationFrame(animate);
}
const newTopic = async () => {
if (!activeAgent.value) return;
creatingTopic.value = true;
try {
const newTopic = await createTopic('New Topic', activeAgent.value.id);
// Navigate to new topic
await navigateTo(`/agent/${activeAgent.value.id}/topic/${newTopic.id}`);
} catch (error) {
console.error('Failed to create topic:', error);
} finally {
creatingTopic.value = false;
}
};
</script>
<template>
<div class="flex flex-col gap-2">
<!-- Header -->
<button @click="toggleAgentsList"
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent hover:bg-[var(--color-highlight)] transition-colors w-full text-left">
<span class="text-sm font-medium">Topics</span>
<Icon name="mynaui:chevron-down" :class="['w-4 h-4', topicsOpen ? '' : '-rotate-90']" />
</button>
<div ref="topicsListRef"
:style="{ height: topicsListHeight, opacity: topicsListOpacity, transform: `scale(${topicsListScale})` }"
class="mt-1 gap-1 flex flex-col overflow-hidden transform-origin-center-top">
<button @click="newTopic" :disabled="creatingTopic"
class="flex items-center gap-2 px-1 h-9 shrink-0 rounded-lg text-sm text-[var(--color-subtle)] bg-transparent hover:bg-[var(--color-highlight)] transition-colors disabled:opacity-50 w-full">
<div class="h-7 w-7 flex items-center justify-center">
<Icon v-if="creatingTopic" class="text-4.5" name="svg-spinners:ring-resize" />
<Icon v-else class="text-4.5" name="mynaui:plus" />
</div>
<span>{{ creatingTopic ? 'Creating...' : 'New Topic' }}</span>
</button>
<NuxtLink v-for="topic in topicsForActiveAgent" :to="`/agent/${activeAgent?.id}/topic/${topic.id}`"
class="decoration-none text-[var(--color-subtle)]">
<SidenavItem :name="topic.name" icon="mynaui:check-hexagon" />
</NuxtLink>
</div>
</div>
</template>
+54 -32
View File
@@ -1,33 +1,33 @@
<script setup lang="ts">
const { agents, createAgent } = await useAgents()
const agentsListRef = ref<HTMLElement | null>(null)
const agentsOpen = ref(true)
const agentsListHeight = ref('auto')
const agentsListOpacity = ref(1)
const agentsListScale = ref(1)
const creatingAgent = ref(false)
const { agents } = await useAgents();
const agentsListRef = ref<HTMLElement | null>(null);
const agentsOpen = ref(true);
const agentsListHeight = ref('auto');
const agentsListOpacity = ref(1);
const agentsListScale = ref(1);
const creatingAgent = ref(false);
function easeInOutQuad(x: number): number {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
return x < 0.5 ? 2 * x * x : 1 - (-2 * x + 2) ** 2 / 2;
}
const toggleAgentsList = () => {
if (!agentsListRef.value) return;
let animationLength = 200;
const animationLength = 200;
let animationStart: number | null = null;
let startHeight: number;
let startOpacity = agentsListOpacity.value;
let startScale = agentsListScale.value;
const startOpacity = agentsListOpacity.value;
const startScale = agentsListScale.value;
if (agentsListHeight.value === 'auto') {
startHeight = agentsListRef.value.clientHeight;
} else {
startHeight = Number(agentsListHeight.value.replace('px', ''));
}
let targetHeight = agentsOpen.value ? 0 : agentsListRef.value.scrollHeight;
let targetOpacity = agentsOpen.value ? 0 : 1;
let targetScale = agentsOpen.value ? 0.95 : 1;
const targetHeight = agentsOpen.value ? 0 : agentsListRef.value.scrollHeight;
const targetOpacity = agentsOpen.value ? 0 : 1;
const targetScale = agentsOpen.value ? 0.95 : 1;
agentsOpen.value = !agentsOpen.value;
const animate = (timestamp: number) => {
@@ -54,36 +54,59 @@ const toggleAgentsList = () => {
};
requestAnimationFrame(animate);
}
};
const newAgent = async () => {
creatingAgent.value = true;
const agent = await createAgent();
creatingAgent.value = false;
navigateTo(`/agent/${agent!.id}`);
}
const triplit = useTriplitClient();
const { user } = useAuth();
if (!user.value) throw new Error('User not logged in');
const agent = await triplit.insert('agents', {
name: 'New Agent',
userId: user.value.id,
systemPrompt: 'You are a helpful assistant.',
imageUrl: null,
createdAt: new Date(),
});
console.log(agents.value, agent);
if (!agent) throw new Error('Failed to create agent');
let agentExists: () => void;
const agentExistsPromise = new Promise<void>((resolve) => {
agentExists = resolve;
});
watch(agents, () => {
agentExists();
});
await agentExistsPromise;
navigateTo(`/agent/${agent.id}`);
};
</script>
<template>
<nav class="flex flex-col gap-1">
<SidenavItem name="Search" icon="mynaui:search" />
<NuxtLink to="/" class="decoration-none text-[var(--color-text)]">
<SidenavItem class="bg-[var(--color-highlight)]" name="Home" icon="mynaui:home" />
</NuxtLink>
<SidenavItem to="/" :active="true" name="Home" icon="mynaui:home" />
<!-- Agents Section -->
<div class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent hover:bg-[var(--color-highlight)] transition-colors w-full text-left"
<button
class="flex items-center justify-between gap-2 px-2 py-2 rounded-lg bg-transparent hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-colors w-full text-left"
@click="toggleAgentsList()">
<span class="text-sm">Agents</span>
<Icon name="mynaui:chevron-down"
:class="['w-4 h-4 transition-transform duration-250 ease-[cubic-bezier(0.5,_1,_0.89,_1)] transform-origin-center', agentsOpen ? '' : '-rotate-90']" />
</div>
</button>
<div ref="agentsListRef"
<div ref="agentsListRef" :inert="!agentsOpen"
:style="{ height: agentsListHeight, opacity: agentsListOpacity, transform: `scale(${agentsListScale})` }"
class="mt-1 gap-1 flex flex-col overflow-hidden transform-origin-center-top">
class="mt-1 gap-1 flex flex-col transform-origin-center-top overflow-y-hidden">
<button @click="newAgent" :disabled="creatingAgent"
class="flex items-center gap-2 px-1 h-9 shrink-0 rounded-lg text-sm text-[var(--color-subtle)] bg-transparent hover:bg-[var(--color-highlight)] transition-colors disabled:opacity-50 w-full">
class="flex items-center gap-2 px-1 h-9 shrink-0 rounded-lg text-sm text-[var(--color-muted)] bg-transparent hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] transition-colors disabled:opacity-50 w-full">
<div class="h-7 w-7 flex items-center justify-center">
<Icon v-if="creatingAgent" class="text-4.5" name="svg-spinners:ring-resize" />
<Icon v-else class="text-4.5" name="mynaui:plus" />
@@ -91,10 +114,9 @@ const newAgent = async () => {
<span>New Agent</span>
</button>
<NuxtLink v-for="agent in agents" :to="`/agent/${agent.id}`" :key="agent.id"
class="decoration-none text-[var(--color-subtle)]">
<SidenavItem :name="agent.name" icon="mynaui:check-hexagon" />
</NuxtLink>
<SidenavItem
v-for="agent in agents?.map((agent) => agent)?.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())"
:to="`/agent/${agent.id}`" :key="agent.id" :name="agent.name" icon="mynaui:check-hexagon" />
</div>
</nav>
</template>
+73 -52
View File
@@ -1,93 +1,105 @@
<script setup lang="ts">
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar()
const route = useRoute()
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar();
const route = useRoute();
const isResizing = ref(false)
const startX = ref(0)
const initialWidth = ref(0)
const isResizing = ref(false);
const startX = ref(0);
const initialWidth = ref(0);
const closeSidenavRef = ref<HTMLElement | null>(null)
const sidenavRef = ref<HTMLElement | null>(null);
const closeSidenavRef = ref<HTMLElement | null>(null);
const { toggle: toggleSettings } = useSettings();
const onResizeStart = (event: MouseEvent) => {
isResizing.value = true
startX.value = event.clientX
initialWidth.value = sidebarWidth.value
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}
isResizing.value = true;
startX.value = event.clientX;
initialWidth.value = sidebarWidth.value;
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
};
const onResizeMove = (event: MouseEvent) => {
if (!isResizing.value) return
if (!isResizing.value) return;
if (resizeAnimationFrame) return;
resizeAnimationFrame = requestAnimationFrame(() => {
const deltaX = event.clientX - startX.value
const newWidth = initialWidth.value + deltaX
resize(newWidth)
resizeAnimationFrame = null
})
}
const deltaX = event.clientX - startX.value;
const newWidth = initialWidth.value + deltaX;
resize(newWidth);
resizeAnimationFrame = null;
});
};
const onResizeEnd = () => {
if (!isResizing.value) return
if (!isResizing.value) return;
isResizing.value = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
saveWidth()
}
isResizing.value = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
saveWidth();
};
let resizeAnimationFrame: number | null = null
let resizeAnimationFrame: number | null = null;
onMounted(() => {
document.addEventListener('mousemove', onResizeMove)
document.addEventListener('mouseup', onResizeEnd)
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeEnd);
watch(hovering, (value) => {
if (!closeSidenavRef.value) return;
if (value) {
const width = closeSidenavRef.value!.scrollWidth
closeSidenavRef.value!.style.width = `${width}px`
const width = closeSidenavRef.value.scrollWidth;
closeSidenavRef.value.style.width = `${width}px`;
} else {
closeSidenavRef.value!.style.width = '0'
closeSidenavRef.value.style.width = '0';
}
})
})
});
});
onUnmounted(() => {
document.removeEventListener('mousemove', onResizeMove)
document.removeEventListener('mouseup', onResizeEnd)
})
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeEnd);
});
const hovering = ref(false)
const hovering = ref(false);
const navKind = computed(() => {
if (route.path === '/') return 'home'
if (route.path.startsWith('/agent/')) return 'agent'
return null
})
if (route.path === '/') return 'home';
if (route.path.startsWith('/agent/')) return 'agent';
return null;
});
const onFocusOut = (e: FocusEvent) => {
const isMovingOutside = sidenavRef.value && !sidenavRef.value.contains(e.relatedTarget as Node);
if (isMovingOutside) {
hovering.value = false;
}
};
</script>
<template>
<div class="relative">
<aside :class="[
'h-full max-w-fit bg-[var(--color-base)] overflow-hidden will-change-width text-[var(--color-subtle)] select-none',
<aside ref="sidenavRef" :class="[
'h-full max-w-fit bg-[var(--color-base)] overflow-hidden will-change-width text-[var(--color-muted)] select-none',
open ? 'w-full mr-2' : 'w-0 mr-0',
isResizing ? '' : 'transition-[width,margin] duration-250 ease-[cubic-bezier(0,0.55,0.45,1)]'
]" :style="open ? { width: `${sidebarWidth}px` } : {}" @mouseenter="hovering = true"
@mouseleave="hovering = false">
@mouseleave="hovering = false" @focusin="hovering = true" @focusout="onFocusOut">
<div :style="{ minWidth: `${sidebarWidth}px` }" class="flex flex-col h-full justify-between">
<div class="flex flex-col">
<div class="flex flex-col h-full max-h-full overflow-y-hidden">
<!-- Header -->
<div class="relative flex flex-row gap-2 justify-between items-center mb-1.5">
<SidenavHeader v-if="navKind === 'home'" v-model="hovering" />
<SidenavHeaderAgent v-else-if="navKind === 'agent'" v-model="hovering" />
<div class="flex items-center justify-end text-[var(--color-subtle)] gap-0.5">
<div class="flex items-center justify-end text-[var(--color-muted)] gap-0.5">
<div ref="closeSidenavRef" style="width: 0;"
:class="['flex-shrink-0 overflow-hidden rounded-lg transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)] transform-origin-center-right']">
<button aria-label="close sidebar" @click="closeSidebar" :class="[
'text-5 p-1.5 hover:bg-[var(--color-highlight)] bg-transparent transition-inherit',
'flex text-5 h-8 w-8 items-center justify-center hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] scale-100 bg-transparent transition-inherit',
]">
<Icon name="mynaui:panel-left-close"
:class="['transition-inherit', hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']" />
@@ -95,7 +107,7 @@ const navKind = computed(() => {
</div>
<div v-if="navKind === 'agent'" class="flex-shrink-0 overflow-hidden rounded-lg">
<NuxtLink aria-label="Start a new topic" :to="`/agent/${route.params.id}`" :class="[
'flex text-5 p-1.5 hover:bg-[var(--color-highlight)] bg-transparent text-inherit',
'flex text-5 h-8 w-8 items-center justify-center hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] bg-transparent text-inherit',
]">
<Icon name="mynaui:book-plus" />
</NuxtLink>
@@ -104,15 +116,24 @@ const navKind = computed(() => {
</div>
<!-- Main Menu -->
<div class="max-h-full overflow-auto">
<div class="max-h-full h-full overflow-auto" style="scrollbar-width: thin;">
<SidenavNavHome v-if="navKind === 'home'" />
<SidenavNavAgent v-else-if="navKind === 'agent'" />
</div>
</div>
<!-- Theme Switcher -->
<div class="flex justify-end">
<ThemeSwitcher />
<div class="flex justify-between pt-2">
<div class="flex">
<button @click="toggleSettings()"
class="flex items-center justify-center h-7 w-7 cursor-pointer hover:bg-[var(--color-highlight)] focus-visible:bg-[var(--color-highlight)] rounded-lg transition-colors text-[var(--color-muted)] active:text-[var(--color-text)]">
<Icon name="mynaui:cog-four" class="text-4" />
</button>
</div>
<!-- Theme Switcher -->
<div class="flex justify-end gap-1">
<ThemeSwitcher />
</div>
</div>
</div>