6ee4087a29
- Centralize `useAgents` and `useModels` state within the Nuxt app context to prevent data leaks and improve initialization. - Migrate virtualization from `vue-virtual-scroller` to `@tanstack/vue-virtual` with new `RowVirtualizerFixed` and `RowVirtualizerDynamic` components. - Upgrade Nuxt to v4.3.1 and remove `@vue-macros/nuxt`. - Replace `big.js` with an optimized custom `lshDecimal` string manipulation logic for pricing calculations in the provider API. - Implement automatic focus redirection in `ChatInput` to capture standard keyboard input. - Refactor Sidenav and Settings components to utilize virtualization for long lists (topics, agents, models). - Enhance theme colors and mobile experience. More work to come on both of these.
132 lines
5.2 KiB
Vue
132 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import GeneralSettings from './GeneralSettings.vue';
|
|
import ProviderSettings from './ProviderSettings.vue';
|
|
import ProviderSidebar from './ProviderSidebar.vue';
|
|
import SystemAssistants from './SystemAssistants.vue';
|
|
import AppearanceSettings from './AppearanceSettings.vue';
|
|
import AIServiceProvider from './AIServiceProvider.vue';
|
|
|
|
const { providers } = useModels();
|
|
|
|
const PAGES_CONFIG = {
|
|
general: {
|
|
label: 'General',
|
|
icon: 'mynaui:cog-four',
|
|
component: GeneralSettings
|
|
},
|
|
appearance: {
|
|
label: 'Appearance',
|
|
icon: 'tabler:palette',
|
|
component: AppearanceSettings
|
|
},
|
|
providers: {
|
|
label: 'AI Providers',
|
|
icon: 'mynaui:api',
|
|
component: ProviderSettings,
|
|
sidebar: ProviderSidebar
|
|
},
|
|
systemAssistants: {
|
|
label: 'System Assistants',
|
|
icon: 'mynaui:sparkles',
|
|
component: SystemAssistants
|
|
},
|
|
} as const;
|
|
|
|
const { currentPage, pageParams, open, setPage, close } = useSettings();
|
|
|
|
const runtimePage = computed(() => {
|
|
// 1. Get the base config (e.g., 'providers' or 'general')
|
|
const config = PAGES_CONFIG[currentPage.value as keyof typeof PAGES_CONFIG] || PAGES_CONFIG.general;
|
|
|
|
// 2. Determine the actual component to show
|
|
let component = config.component;
|
|
let label = config.label as string;
|
|
|
|
if (currentPage.value === 'providers' && pageParams.value.length > 0) {
|
|
component = AIServiceProvider;
|
|
const providerId = pageParams.value[0];
|
|
const provider = providers.value!.find(p => p.id === providerId);
|
|
label = provider ? provider.name : 'Unknown Provider';
|
|
}
|
|
|
|
return {
|
|
...config,
|
|
label,
|
|
component,
|
|
params: pageParams.value,
|
|
} as {
|
|
label: string;
|
|
icon: string;
|
|
component: Component;
|
|
sidebar?: Component;
|
|
params: string[];
|
|
};
|
|
});
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
close();
|
|
}
|
|
};
|
|
|
|
watch(open, (value) => {
|
|
if (value) {
|
|
document.body.addEventListener('keydown', handleKeyDown);
|
|
} else {
|
|
document.body.removeEventListener('keydown', handleKeyDown);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (open.value) {
|
|
document.body.removeEventListener('keydown', handleKeyDown);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<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 backdrop-blur-md" @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-[85vw] max-w-6xl h-[70vh] bg-[var(--bg-base)] rounded-2xl shadow-2xl border border-[var(--color-border)]
|
|
overflow-hidden flex max-h-[90vh] p-2">
|
|
<nav class="w-64 flex flex-col gap-1 mr-2 overflow-y-auto">
|
|
<!-- If the page has a custom sidebar (for nested lists), show it; otherwise show default nav -->
|
|
<component v-if="runtimePage?.sidebar" :is="runtimePage.sidebar" @navigate="setPage" />
|
|
|
|
<button v-else v-for="(config, id) in PAGES_CONFIG" :key="id" @click="setPage(id)"
|
|
:class="[currentPage === id ? 'bg-[var(--color-hover)]' : 'hover:bg-[var(--color-hover)]', 'flex justify-between items-center shrink-0 px-1 rounded-lg transition-colors cursor-pointer h-9']">
|
|
<div class="flex items-center gap-2 max-w-full flex-1">
|
|
<Icon :name="config.icon" class="w-5 h-5" />
|
|
{{ config.label }}
|
|
</div>
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- DYNAMIC CONTENT -->
|
|
<main class="flex-1 flex flex-col overflow-hidden">
|
|
<div
|
|
class="max-h-full h-full flex flex-col flex-1 px-3 bg-[var(--bg-surface)] border rounded-lg border-[var(--color-border)]">
|
|
<header class="flex items-center justify-between pl-2 pb-2 pt-2 ">
|
|
<h2 class="text-lg font-semibold m-0 capitalize">{{ runtimePage.label }}</h2>
|
|
<button
|
|
class="hover:bg-[var(--color-hover)] p-1.5 rounded-md transition-colors duration-200 ease-[cubic-bezier(0,0.55,0.45,1)]"
|
|
@click="close">
|
|
<Icon name="mynaui:x-solid" />
|
|
</button>
|
|
</header>
|
|
<component @navigate="setPage" :is="runtimePage.component" />
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|