continue scaffolding and refine the basic foundation
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownItem } from '~/types/dropdown';
|
||||
|
||||
interface Props {
|
||||
items: DropdownItem[];
|
||||
modelValue?: boolean;
|
||||
placement?: 'right' | 'left' | 'center';
|
||||
verticality?: 'asscending' | 'descending';
|
||||
width?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
placement: 'right',
|
||||
verticality: 'descending',
|
||||
width: 'auto'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'select', item: DropdownItem): void;
|
||||
}>()
|
||||
|
||||
const triggerRef = ref<HTMLElement | null>(null)
|
||||
const isOpen = defineModel<boolean>({ required: true })
|
||||
|
||||
const toggle = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
const select = (item: DropdownItem) => {
|
||||
if (item.disabled || item.divider) return
|
||||
emit('select', item)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const placementClasses = computed(() => {
|
||||
let classes = ''
|
||||
|
||||
switch (props.placement) {
|
||||
case 'right':
|
||||
classes += 'right-0 '
|
||||
break
|
||||
case 'left':
|
||||
classes += 'left-0 '
|
||||
break
|
||||
case 'center':
|
||||
classes += 'left-1/2 -translate-x-1/2 '
|
||||
break
|
||||
default:
|
||||
classes += 'left-0 '
|
||||
break
|
||||
}
|
||||
|
||||
switch (props.verticality) {
|
||||
case 'asscending':
|
||||
classes += 'bottom-full mb-1.5'
|
||||
break
|
||||
case 'descending':
|
||||
classes += 'top-full mt-1.5'
|
||||
break
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
useClickOutside(triggerRef, () => {
|
||||
isOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="triggerRef" :class="$attrs.class">
|
||||
<slot name="trigger" :toggle="toggle" :is-open="isOpen"></slot>
|
||||
|
||||
<Transition enter-active-class="transition-all duration-150 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
enter-from-class="opacity-0 scale-95" enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition-all duration-100 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
||||
<div v-if="isOpen" :class="[
|
||||
'absolute z-50 bg-[var(--color-neutral)] border border-[var(--color-highlight)] rounded-xl p-1.5 flex flex-col gap-1',
|
||||
placementClasses
|
||||
]" :style="{ width: width !== 'auto' ? width : undefined }">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<div v-if="item.divider" class="h-px bg-[var(--color-highlight)] my-1" />
|
||||
<button @click="select(item); item.onClick && item.onClick()" :disabled="item.disabled"
|
||||
class="bg-transparent w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-colors text-left"
|
||||
:class="[
|
||||
item.disabled
|
||||
? 'opacity-50 cursor-not-allowed'
|
||||
: 'hover:bg-[var(--color-highlight)] cursor-pointer'
|
||||
]">
|
||||
<Icon v-if="item.icon" :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="text-sm whitespace-nowrap">{{ item.label }}</span>
|
||||
<slot name="item-after" :item="item"></slot>
|
||||
</button>
|
||||
</template>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { Message } from '~~/types';
|
||||
|
||||
interface Props {
|
||||
message: Message;
|
||||
regenerations?: Message[];
|
||||
isCurrent?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
regenerations: () => [],
|
||||
isCurrent: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
regenerate: [messageId: string];
|
||||
select: [messageId: string];
|
||||
delete: [messageId: string];
|
||||
}>()
|
||||
|
||||
const isOpen = ref(false)
|
||||
const content = computed(() => props.message.content)
|
||||
const hasAlternatives = computed(() => props.regenerations.length > 0)
|
||||
const showDropdown = computed(() => hasAlternatives.value || !props.isUser)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-3 relative group">
|
||||
<div
|
||||
class="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center"
|
||||
:class="isUser ? 'bg-[var(--color-accent)]' : 'bg-[var(--color-neutral)] border border-[var(--color-highlight)]'"
|
||||
>
|
||||
<Icon
|
||||
:name="isUser ? 'mynaui:user' : 'mynaui:check-hexagon'"
|
||||
class="w-4 h-4"
|
||||
:class="isUser ? 'text-[var(--color-accent-text)]' : 'text-[var(--color-accent)]'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<p class="text-sm font-medium" :class="isUser ? 'text-[var(--color-accent)]' : 'text-[var(--color-neutral)]'">
|
||||
{{ isUser ? 'You' : 'Agent' }}
|
||||
</p>
|
||||
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Dropdown v-if="showDropdown" v-model="isOpen" placement="bottom-right" width="140px">
|
||||
<template #trigger="{ toggle }">
|
||||
<button
|
||||
@click="toggle"
|
||||
class="p-1 rounded hover:bg-[var(--color-highlight)]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<Icon name="mynaui:dots-horizontal" class="w-4 h-4 text-[var(--color-subtle)]" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<button
|
||||
v-if="!isUser"
|
||||
@click="emit('regenerate', message.id)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-[var(--color-highlight)] text-left"
|
||||
>
|
||||
<Icon name="mynaui:refresh" class="w-4 h-4" />
|
||||
<span class="text-sm">Regenerate</span>
|
||||
</button>
|
||||
<div v-if="!isUser && hasAlternatives" class="h-px bg-[var(--color-highlight)] my-1" />
|
||||
<template v-if="hasAlternatives">
|
||||
<button
|
||||
v-for="alt in regenerations"
|
||||
:key="alt.id"
|
||||
@click="emit('select', alt.id)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-[var(--color-highlight)] text-left"
|
||||
:class="message.id === alt.id ? 'bg-[var(--color-highlight)]' : ''"
|
||||
>
|
||||
<Icon name="mynaui:clock" class="w-4 h-4 text-[var(--color-subtle)]" />
|
||||
<span class="text-sm text-[var(--color-subtle)]">{{ alt.id }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="h-px bg-[var(--color-highlight)] my-1" />
|
||||
<button
|
||||
@click="emit('delete', message.id)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-[var(--color-highlight)] text-left text-red-400"
|
||||
>
|
||||
<Icon name="mynaui:trash" class="w-4 h-4" />
|
||||
<span class="text-sm">Delete</span>
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-[var(--color-text)] whitespace-pre-wrap break-words">{{ content }}</p>
|
||||
|
||||
<div v-if="editedAt" class="mt-1 text-xs text-[var(--color-subtle)] flex items-center gap-1">
|
||||
<Icon name="mynaui:pencil" class="w-3 h-3" />
|
||||
<span>Edited</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,67 +13,79 @@ const simulateTask = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="open" class="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/80"
|
||||
@click.self="close">
|
||||
<div class="w-[70vw] max-w-6xl h-[70vh] bg-[var(--color-base)] rounded-xl shadow-2xl border border-[var(--color-highlight)]
|
||||
<Transition class="transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]" enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
|
||||
<div v-if="open" class="fixed inset-0 z-45 bg-black/80" @click.self="close">
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition class="transition-all duration-200 ease-[cubic-bezier(0.5,_1,_0.89,_1)]"
|
||||
enter-from-class="opacity-0 scale-95 translate-y-2" leave-from-class="opacity-100 scale-100 translate-y-0"
|
||||
enter-to-class="opacity-100 scale-100 translate-y-0" leave-to-class="opacity-0 scale-95 -translate-y-2">
|
||||
<div v-if="open" class="z-50 fixed top-1/2 left-1/2 -translate-x-1/2 flex items-center justify-center">
|
||||
<div class="absolute w-[70vw] max-w-6xl h-[70vh] bg-[var(--color-base)] rounded-xl shadow-2xl border border-[var(--color-highlight)]
|
||||
overflow-hidden flex max-h-[90vh] p-2">
|
||||
<!-- Sidebar Nav -->
|
||||
<nav class="w-64 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between pb-4">
|
||||
<div class="flex items-center gap-2 px-2">
|
||||
<Icon name="mynaui:cog-four" class="w-7 h-7 text-[var(--color-subtle)] mt-1" />
|
||||
<h1 class="font-semibold text-center">Settings</h1>
|
||||
<!-- Sidebar Nav -->
|
||||
<nav class="w-64 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between pb-4">
|
||||
<div class="flex items-center gap-2 px-2">
|
||||
<Icon name="mynaui:cog-four" class="w-7 h-7 text-[var(--color-subtle)] mt-1" />
|
||||
<h1 class="font-semibold text-center">Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="page in pages" :key="page" :class="[
|
||||
'select-none cursor-pointer flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors w-full text-left',
|
||||
currentPage === page ? 'bg-[var(--color-highlight)]' : 'hover:bg-[var(--color-highlight)]/10'
|
||||
]" @click="setPage(page)">
|
||||
{{ page.charAt(0).toUpperCase() + page.slice(1) }}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<header class="flex items-center justify-between pl-2 pb-2">
|
||||
<h2 class="text-lg font-semibold">{{ currentPage.charAt(0).toUpperCase() + currentPage.slice(1) }}
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="settings-loader-target"></div>
|
||||
<button @click="close"
|
||||
class="p-1.5 rounded-lg text-[var(--color-text)] bg-transparent hover:bg-[var(--color-highlight)]/10 transition-colors">
|
||||
<Icon name="mynaui:x-solid" class="w-5 h-5" />
|
||||
</button>
|
||||
<div v-for="page in pages" :key="page" :class="[
|
||||
'select-none cursor-pointer flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors w-full text-left',
|
||||
currentPage === page ? 'bg-[var(--color-highlight)]' : 'hover:bg-[var(--color-highlight)]/10'
|
||||
]" @click="setPage(page)">
|
||||
{{ page.charAt(0).toUpperCase() + page.slice(1) }}
|
||||
</div>
|
||||
</header>
|
||||
</nav>
|
||||
|
||||
<div
|
||||
class="flex-1 p-6 ml-1 mt-1 bg-[var(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
<div v-if="currentPage === 'page1'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 1 content. Try adding a task to
|
||||
the queue
|
||||
below.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="accent" @click="simulateTask">
|
||||
Simulate Task (2s)
|
||||
<!-- Content -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<header class="flex items-center justify-between pl-2 pb-2">
|
||||
<h2 class="text-lg font-semibold">{{ currentPage.charAt(0).toUpperCase() + currentPage.slice(1)
|
||||
}}
|
||||
</h2>
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="settings-loader-target"></div>
|
||||
<button @click="close"
|
||||
class="p-1.5 rounded-lg text-[var(--color-text)] bg-transparent hover:bg-[var(--color-highlight)]/10 transition-colors">
|
||||
<Icon name="mynaui:x-solid" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="currentPage === 'page2'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 2 content with some details.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div class="p-4 bg-[var(--color-highlight-low)] rounded-lg text-sm">Item 1</div>
|
||||
<div class="p-4 bg-[var(--color-highlight-low)] rounded-lg text-sm">Item 2</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="flex-1 p-6 ml-1 mt-1 bg-[var(--color-neutral)] overflow-y-auto border rounded-lg border-[var(--color-highlight)]">
|
||||
<div v-if="currentPage === 'page1'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 1 content. Try adding a
|
||||
task to
|
||||
the queue
|
||||
below.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<button class="accent" @click="simulateTask">
|
||||
Simulate Task (2s)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="currentPage === 'page2'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 2 content with some
|
||||
details.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4">
|
||||
<div class="p-4 bg-[var(--color-highlight-low)] rounded-lg text-sm">Item 1</div>
|
||||
<div class="p-4 bg-[var(--color-highlight-low)] rounded-lg text-sm">Item 2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="currentPage === 'page3'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 3 content.</p>
|
||||
<button class="accent" @click="simulateTask">Run Task</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="currentPage === 'page3'">
|
||||
<p class="text-sm text-[var(--color-subtle)] mb-4">Settings page 3 content.</p>
|
||||
<button class="accent" @click="simulateTask">Run Task</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownItem } from '~/types/dropdown'
|
||||
|
||||
type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const isOpen = ref(false)
|
||||
const buttonRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const themeOptions: { value: Theme; label: string; icon: string }[] = [
|
||||
const themeOptions: DropdownItem[] = [
|
||||
{ value: 'light', label: 'Light', icon: 'mynaui:sun' },
|
||||
{ value: 'dark', label: 'Dark', icon: 'mynaui:moon' },
|
||||
{ value: 'system', label: 'System', icon: 'mynaui:desktop' }
|
||||
@@ -15,38 +15,23 @@ const currentOption = computed(() =>
|
||||
themeOptions.find(option => option.value === colorMode.preference) || themeOptions[2]
|
||||
)
|
||||
|
||||
const selectTheme = (newTheme: Theme) => {
|
||||
colorMode.preference = newTheme
|
||||
isOpen.value = false
|
||||
}
|
||||
const isOpen = ref(false)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
const selectTheme = (item: DropdownItem) => {
|
||||
colorMode.preference = item.value as Theme
|
||||
}
|
||||
|
||||
useClickOutside(buttonRef, () => {
|
||||
isOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="buttonRef" class="relative">
|
||||
<button aria-label="Open theme switcher" @click="toggleDropdown"
|
||||
class="p-2 flex items-center justify-center rounded-lg hover:bg-[var(--color-highlight)] transition-colors group"
|
||||
:class="isOpen ? 'bg-[var(--color-highlight)]' : 'bg-transparent'">
|
||||
<Icon :name="currentOption!.icon"
|
||||
class="text-5 text-[var(--color-subtle)] group-hover:text-[var(--color-text)] transition-colors" />
|
||||
</button>
|
||||
|
||||
<div v-if="isOpen"
|
||||
class="flex flex-col gap-1 absolute bottom-full mb-2 right-0 bg-[var(--color-neutral)] border border-[var(--color-highlight)] rounded-lg p-1 min-w-[120px] shadow-lg">
|
||||
<button v-for="option in themeOptions" :key="option.value" @click="selectTheme(option.value)"
|
||||
class="w-full flex items-center justify-left gap-2 px-3 py-2 rounded-md hover:bg-[var(--color-highlight)] transition-colors"
|
||||
:class="colorMode.preference === option.value ? 'bg-[var(--color-highlight)] text-[var(--color-text)]' :
|
||||
'bg-transparent text-[var(--color-subtle)]'">
|
||||
<Icon :name="option.icon" class="w-4 h-4" />
|
||||
<span class="text-sm">{{ option.label }}</span>
|
||||
<Dropdown class="relative" v-model="isOpen" @select="selectTheme" :items="themeOptions" verticality="asscending"
|
||||
placement="right" width="140px">
|
||||
<template #trigger="{ toggle, isOpen }">
|
||||
<button aria-label="Open theme switcher" @click="toggle"
|
||||
class="p-2 flex items-center justify-center rounded-lg hover:bg-[var(--color-highlight)] transition-colors group"
|
||||
:class="isOpen ? 'bg-[var(--color-highlight)]' : 'bg-transparent'">
|
||||
<Icon :name="currentOption!.icon!"
|
||||
class="text-5 text-[var(--color-subtle)] group-hover:text-[var(--color-text)] transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</template>
|
||||
Reference in New Issue
Block a user