continue scaffolding and refine the basic foundation

This commit is contained in:
Zoe
2026-01-13 19:56:24 +00:00
parent 0877cc10bd
commit 8c28946703
37 changed files with 1431 additions and 509 deletions
+29 -34
View File
@@ -1,51 +1,46 @@
<script setup lang="ts">
import type { DropdownItem } from '~/types/dropdown'
const { user, signOut } = useAuth()
const { toggle: toggleSettings } = useSettings()
const profileRef = ref<HTMLElement | null>(null)
const profileOpen = ref(false)
const hovering = defineModel<boolean>({ required: true })
const toggleProfile = () => {
profileOpen.value = !profileOpen.value
}
const profileOpen = ref(false)
const handleLogout = async () => {
await signOut()
profileOpen.value = false
}
useClickOutside(profileRef, () => {
profileOpen.value = false
})
const profileItems: DropdownItem[] = [
{ label: 'Settings', icon: 'mynaui:cog-four', onClick: toggleSettings },
{ label: 'Log out', icon: 'mynaui:logout', divider: true, onClick: handleLogout },
]
</script>
<template>
<header class="flex items-center justify-between overflow-hidden">
<div role="button" aria-label="open user dropdown" ref="profileRef"
class="flex items-center gap-1.5 pr-2 rounded-xl hover:bg-[var(--color-highlight)] cursor-pointer transition-colors max-w-full"
@click="toggleProfile">
<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)]" />
</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',
hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-x-0 scale-y-90'
]">
<Icon class="text-4" name="mynaui:chevron-down" />
</div>
</div>
<!-- Profile Dropdown -->
<div v-show="profileOpen"
class="absolute top-full left-0 right-0 z-50 mt-1.5 bg-[var(--color-neutral)] border border-[var(--color-highlight)] rounded-xl p-2 w-full gap-1 flex flex-col text-[var(--color-text)]">
<SidenavItem @click="toggleSettings(); profileOpen = false" name="Settings" icon="mynaui:cog-four" />
<SidenavItem @click="handleLogout" name="Log out" icon="mynaui:logout" />
</div>
<Dropdown 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"
@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)]" />
</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',
hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-x-0 scale-y-90'
]">
<Icon class="text-4" name="mynaui:chevron-down" />
</div>
</div>
</template>
</Dropdown>
</header>
</template>
+37 -34
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import type { DropdownItem } from '~/types/dropdown'
const { user } = useAuth()
const { agents, activeAgent } = await useAgents()
const homeButtonRef = ref<HTMLElement | null>(null)
const agentDropdownRef = ref<HTMLElement | null>(null)
const agentDropdownOpen = ref(false)
const hovering = defineModel<boolean>({ required: true })
const initialized = ref(false)
@@ -26,11 +26,11 @@ onMounted(() => {
homeButtonRef.value!.style.width = '0'
}
})
});
useClickOutside(agentDropdownRef, () => {
agentDropdownOpen.value = false
})
const agentDropdownOpen = ref(false)
const agentItems: DropdownItem[] = []
</script>
<template>
@@ -43,33 +43,36 @@ useClickOutside(agentDropdownRef, () => {
</NuxtLink>
</div>
<div class="flex overflow-hidden gap-1.5 pr-2 items-center cursor-pointer hover:bg-[var(--color-highlight)] rounded-lg"
ref="agentDropdownRef" @click="agentDropdownOpen = !agentDropdownOpen">
<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="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(--color-text)] whitespace-nowrap">
{{ activeAgent?.name }}
</span>
<div class="w-4 h-4 text-[var(--color-subtle)]">
<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>
<!-- Agent Dropdown -->
<div v-show="agentDropdownOpen" class="absolute top-full left-1/10 z-50 mt-1.5 bg-[var(--color-neutral)] border border-[var(--color-highlight)]
rounded-xl p-2 w-8/10">
<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>
</div>
</div>
<Dropdown 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"
@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)]']">
<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(--color-text)] whitespace-nowrap">
{{ activeAgent?.name }}
</span>
<div class="w-4 h-4 text-[var(--color-subtle)]">
<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>
</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>
</div>
</template>
</Dropdown>
</header>
</template>
+15 -11
View File
@@ -1,32 +1,36 @@
<script setup lang="ts">
const route = useRoute()
const route = useRoute();
const routeParts = computed(() => {
return route.path.replace('/agent/', '').split('/')
return route.path.replace('/agent/', '').split('/');
});
if (routeParts.value.length < 1) navigateTo('/')
if (!routeParts.value[0]!.match(/^agents_[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/)) navigateTo('/')
if (routeParts.value.length < 1) navigateTo('/');
const pageInfo = computed(() => {
// `/agent/agent_[uuid]`
if (routeParts.value.length === 1) {
return 'new-conversation'
// `/agent/agent_[uuid]` or `/agent/agent_[uuid]/topic/...`
if (route.path.startsWith('/agent/') && !route.path.includes('/profile')) {
return 'conversation';
}
// `/agent/agent_[uuid]/profile`
if (routeParts.value.length === 2 && routeParts.value[1]! === 'profile') {
return 'agent-profile'
if (route.path.endsWith('/profile')) {
return 'agent-profile';
}
})
});
</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>
<!-- Topics Section -->
<SidenavNavAgentTopics />
</nav>
</template>
</template>
+106
View File
@@ -0,0 +1,106 @@
<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>
+22 -24
View File
@@ -1,6 +1,5 @@
<script setup lang="ts">
const { agents, createAgent } = await useAgents()
const route = useRoute()
const agentsListRef = ref<HTMLElement | null>(null)
const agentsOpen = ref(true)
const agentsListHeight = ref('auto')
@@ -73,30 +72,29 @@ const newAgent = async () => {
</NuxtLink>
<!-- Agents Section -->
<div class="relative group">
<div class="flex items-center justify-between px-2 h-9 rounded-lg hover:bg-[var(--color-highlight)] transition-colors cursor-pointer"
@click="toggleAgentsList()">
<div class="flex items-center gap-0.5">
<span class="text-sm">Agents</span>
<Icon name="mynaui:chevron-right-solid"
:class="['transition-transform duration-200 ease-in-out transform-origin-center', agentsOpen ? 'rotate-90' : '']" />
</div>
<button aria-label="create new agent"
class="opacity-0 group-hover:opacity-100 p-1 rounded-md transition-all bg-transparent hover:bg-[var(--color-highlight)] text-[var(--color-subtle)] active:text-[var(--color-text)]"
@click.stop="newAgent">
<Icon v-if="!creatingAgent" name="mynaui:plus" class="w-4 h-4" />
<Icon v-else name="svg-spinners:ring-resize" class="w-4 h-4" />
</button>
</div>
<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"
@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>
<div ref="agentsListRef"
:style="{ height: agentsListHeight, opacity: agentsListOpacity, transform: `scale(${agentsListScale})` }"
class="mt-1 gap-1 flex flex-col overflow-hidden transform-origin-center-top">
<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>
</div>
<div ref="agentsListRef"
:style="{ height: agentsListHeight, opacity: agentsListOpacity, transform: `scale(${agentsListScale})` }"
class="mt-1 gap-1 flex flex-col overflow-hidden transform-origin-center-top">
<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">
<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" />
</div>
<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>
</div>
</nav>
</template>
+2 -1
View File
@@ -78,7 +78,8 @@ const navKind = computed(() => {
<div :style="{ minWidth: `${sidebarWidth}px` }" class="flex flex-col h-full justify-between">
<div class="flex flex-col">
<!-- Header -->
<div class="relative flex flex-row gap-2 justify-between items-center pb-1.5">
<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" />