initial commit
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
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 handleLogout = async () => {
|
||||
await signOut()
|
||||
profileOpen.value = false
|
||||
}
|
||||
|
||||
useClickOutside(profileRef, () => {
|
||||
profileOpen.value = false
|
||||
})
|
||||
</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>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
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)
|
||||
|
||||
onMounted(() => {
|
||||
if (hovering.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 (value) {
|
||||
const width = homeButtonRef.value!.scrollWidth
|
||||
homeButtonRef.value!.style.width = `calc(${width}px + 0.5rem)`
|
||||
} else {
|
||||
homeButtonRef.value!.style.width = '0'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
useClickOutside(agentDropdownRef, () => {
|
||||
agentDropdownOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="flex items-center rounded-lg overflow-hidden">
|
||||
<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">
|
||||
<Icon name="mynaui:chevron-left" class="w-4.5 h-4.5" />
|
||||
</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>
|
||||
</header>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
name: string,
|
||||
icon: 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" />
|
||||
</div>
|
||||
<span class="text-sm font-medium overflow-hidden text-ellipsis">{{ props.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const routeParts = computed(() => {
|
||||
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('/')
|
||||
|
||||
const pageInfo = computed(() => {
|
||||
// `/agent/agent_[uuid]`
|
||||
if (routeParts.value.length === 1) {
|
||||
return 'new-conversation'
|
||||
}
|
||||
|
||||
// `/agent/agent_[uuid]/profile`
|
||||
if (routeParts.value.length === 2 && routeParts.value[1]! === 'profile') {
|
||||
return 'agent-profile'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex flex-col gap-1">
|
||||
<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>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,102 @@
|
||||
<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')
|
||||
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;
|
||||
}
|
||||
|
||||
const toggleAgentsList = () => {
|
||||
if (!agentsListRef.value) return;
|
||||
let animationLength = 200;
|
||||
let animationStart: number | null = null;
|
||||
|
||||
let startHeight: number;
|
||||
let startOpacity = agentsListOpacity.value;
|
||||
let 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;
|
||||
agentsOpen.value = !agentsOpen.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);
|
||||
|
||||
agentsListOpacity.value = currentOpacity;
|
||||
agentsListScale.value = currentScale;
|
||||
agentsListHeight.value = `${currentHeight}px`;
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
if (agentsOpen.value) {
|
||||
agentsListHeight.value = 'auto';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
const newAgent = async () => {
|
||||
creatingAgent.value = true;
|
||||
const agent = await createAgent();
|
||||
creatingAgent.value = false;
|
||||
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>
|
||||
|
||||
<!-- 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 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>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar()
|
||||
const route = useRoute()
|
||||
|
||||
const isResizing = ref(false)
|
||||
const startX = ref(0)
|
||||
const initialWidth = ref(0)
|
||||
|
||||
const closeSidenavRef = ref<HTMLElement | null>(null)
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
const onResizeMove = (event: MouseEvent) => {
|
||||
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 onResizeEnd = () => {
|
||||
if (!isResizing.value) return
|
||||
|
||||
isResizing.value = false
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
saveWidth()
|
||||
}
|
||||
|
||||
let resizeAnimationFrame: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', onResizeMove)
|
||||
document.addEventListener('mouseup', onResizeEnd)
|
||||
|
||||
watch(hovering, (value) => {
|
||||
if (value) {
|
||||
const width = closeSidenavRef.value!.scrollWidth
|
||||
closeSidenavRef.value!.style.width = `${width}px`
|
||||
} else {
|
||||
closeSidenavRef.value!.style.width = '0'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', onResizeMove)
|
||||
document.removeEventListener('mouseup', onResizeEnd)
|
||||
})
|
||||
|
||||
const hovering = ref(false)
|
||||
|
||||
const navKind = computed(() => {
|
||||
if (route.path === '/') return 'home'
|
||||
if (route.path.startsWith('/agent/')) return 'agent'
|
||||
return null
|
||||
})
|
||||
</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',
|
||||
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">
|
||||
<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">
|
||||
<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 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',
|
||||
]">
|
||||
<Icon name="mynaui:panel-left-close"
|
||||
:class="['transition-inherit', hovering ? 'opacity-100 scale-100' : 'opacity-0 scale-95']" />
|
||||
</button>
|
||||
</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',
|
||||
]">
|
||||
<Icon name="mynaui:book-plus" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Menu -->
|
||||
<div class="max-h-full overflow-auto">
|
||||
<SidenavNavHome v-if="navKind === 'home'" />
|
||||
<SidenavNavAgent v-else-if="navKind === 'agent'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Switcher -->
|
||||
<div class="flex justify-end">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
<!-- resize handle -->
|
||||
<div @mousedown="onResizeStart" class="absolute top-0 right-0 bottom-0 p-1 cursor-ew-resize">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user