feat: add message editing
This commit adds message editing as a feature. To accomplish this, the settings dialog was refactored into a singleton and now the message edit and settings use the singleton where appropriate.
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import GeneralSettings from '~/components/Settings/GeneralSettings.vue';
|
||||||
|
import ProviderSettings from '~/components/Settings/ProviderSettings.vue';
|
||||||
|
import ProviderSidebar from '~/components/Settings/ProviderSidebar.vue';
|
||||||
|
import SystemAssistants from '~/components/Settings/SystemAssistants.vue';
|
||||||
|
import AppearanceSettings from '~/components/Settings/AppearanceSettings.vue';
|
||||||
|
import AIServiceProvider from '~/components/Settings/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, 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[];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-2 flex w-full">
|
||||||
|
<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-y-hidden max-h-full h-full 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" />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
title?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirm', 'cancel']);
|
||||||
|
const text = ref(props.initialValue || '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<h3 class="m-4" v-if="title">{{ title }}</h3>
|
||||||
|
<textarea v-model="text" class="bg-[var(--bg-surface)] resize-none h-full w-full p-2 rounded" />
|
||||||
|
<div class="flex justify-end gap-2 m-2">
|
||||||
|
<button
|
||||||
|
class="px-2 py-1 rounded-lg border border-[var(--color-border)] hover:bg-[var(--color-hover)] transition-[background-color] duration-200"
|
||||||
|
@click="emit('cancel')">Cancel</button>
|
||||||
|
<button
|
||||||
|
class="px-2 py-1 rounded-lg bg-[var(--color-accent)] text-[var(--color-accent-text)] hover:bg-[var(--color-accent-hover)] transition-[background-color] duration-200"
|
||||||
|
@click="emit('confirm', text)">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Textbox from './Textbox.vue';
|
||||||
|
import Settings from './Settings.vue';
|
||||||
|
|
||||||
|
import { DialogType } from '~/composables/useDialog';
|
||||||
|
|
||||||
|
const { open, page, data, close, confirm } = useDialog();
|
||||||
|
|
||||||
|
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]">
|
||||||
|
<KeepAlive>
|
||||||
|
<Settings v-if="page === DialogType.Settings" />
|
||||||
|
|
||||||
|
<Textbox v-else-if="page === DialogType.Textbox" v-bind="data" @confirm="confirm" @cancel="close" />
|
||||||
|
</KeepAlive>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Message } from '~/composables/useChat';
|
import type { Message } from '~/composables/useChat';
|
||||||
|
import { assert } from '~~/utils/assert';
|
||||||
|
|
||||||
const triplit = useTriplitClient();
|
const triplit = useTriplitClient();
|
||||||
|
const { openDialog } = useDialog();
|
||||||
|
|
||||||
const { message } = defineProps<{
|
const { message } = defineProps<{
|
||||||
message: Message
|
message: Message
|
||||||
@@ -88,6 +90,26 @@ const regenerateMessage = async () => {
|
|||||||
emit('regenerate');
|
emit('regenerate');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleEdit = async () => {
|
||||||
|
openDialog<string>(DialogType.Textbox, async (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
await triplit.update('messages', message.id, {
|
||||||
|
content: res.data
|
||||||
|
});
|
||||||
|
|
||||||
|
assert('flush' in triplit);
|
||||||
|
await triplit.flush();
|
||||||
|
|
||||||
|
await regenerateMessage();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
title: 'Edit Message',
|
||||||
|
initialValue: message.content
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deleteMessage = () => {
|
const deleteMessage = () => {
|
||||||
if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
if (focusedIndex.value !== 0 && focusedIndex.value === message.children.length) {
|
||||||
focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
focusedIndex.value = Math.max(0, focusedIndex.value - 1);
|
||||||
@@ -135,6 +157,10 @@ const messageCount = computed(() => {
|
|||||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-hover)]">
|
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-hover)]">
|
||||||
<Icon name="mynaui:refresh" class="text-4.5" />
|
<Icon name="mynaui:refresh" class="text-4.5" />
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="message.role === 'user'" @click="handleEdit"
|
||||||
|
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-hover)]">
|
||||||
|
<Icon name="mynaui:pencil" class="text-4.5" />
|
||||||
|
</button>
|
||||||
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
|
<button @click="copyMessage" :class="{ 'text-emerald-500': copied }"
|
||||||
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-hover)]">
|
class="flex justify-center items-center w-7 h-6 hover:bg-[var(--color-hover)]">
|
||||||
<Icon :name="copied ? 'mynaui:check' : 'mynaui:copy'" class="text-4.5" />
|
<Icon :name="copied ? 'mynaui:check' : 'mynaui:copy'" class="text-4.5" />
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { DialogType } from '~/composables/useDialog';
|
||||||
|
|
||||||
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar();
|
const { close: closeSidebar, open, sidebarWidth, resize, saveWidth } = useSidebar();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ const onFocusOut = (e: FocusEvent) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { toggle: toggleSettings } = useSettings();
|
const { openDialog } = useDialog();
|
||||||
|
|
||||||
const onResizeStart = (event: MouseEvent) => {
|
const onResizeStart = (event: MouseEvent) => {
|
||||||
isResizing.value = true;
|
isResizing.value = true;
|
||||||
@@ -141,7 +143,7 @@ const navKind = computed(() => {
|
|||||||
|
|
||||||
<div class="flex justify-between pt-2">
|
<div class="flex justify-between pt-2">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button @click="toggleSettings()"
|
<button @click="openDialog(DialogType.Settings)"
|
||||||
class="flex items-center justify-center h-7 w-7 cursor-pointer hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg transition-colors text-[var(--text-secondary)] active:text-[var(--text-primary)]">
|
class="flex items-center justify-center h-7 w-7 cursor-pointer hover:bg-[var(--color-hover)] focus-visible:bg-[var(--color-hover)] rounded-lg transition-colors text-[var(--text-secondary)] active:text-[var(--text-primary)]">
|
||||||
<Icon name="mynaui:cog-four" class="text-5" />
|
<Icon name="mynaui:cog-four" class="text-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Err, Ok, type Result } from "~~/types/result";
|
||||||
|
|
||||||
|
export enum DialogType {
|
||||||
|
Settings = 'settings',
|
||||||
|
Textbox = 'textbox',
|
||||||
|
Confirm = 'confirm',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DialogOptions {
|
||||||
|
title?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
let actionCallback: ((value: any) => void) | undefined;
|
||||||
|
|
||||||
|
export const useDialog = () => {
|
||||||
|
const page = useState<DialogType | null>('dialog:page', () => null);
|
||||||
|
const open = useState('dialog:open', () => false);
|
||||||
|
const data = useState<DialogOptions>('dialog:data', () => ({}));
|
||||||
|
|
||||||
|
const openDialog = <T = any>(type: DialogType, cb?: (value: Result<T, string>) => void, options: DialogOptions = {}) => {
|
||||||
|
page.value = type;
|
||||||
|
data.value = options;
|
||||||
|
open.value = true;
|
||||||
|
actionCallback = cb;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = (result: any) => {
|
||||||
|
open.value = false;
|
||||||
|
if (actionCallback === undefined) return;
|
||||||
|
|
||||||
|
actionCallback?.(Ok(result));
|
||||||
|
actionCallback = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
open.value = false;
|
||||||
|
if (actionCallback === undefined) return;
|
||||||
|
|
||||||
|
actionCallback?.(Err('closed'));
|
||||||
|
actionCallback = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: readonly(open),
|
||||||
|
page: readonly(page),
|
||||||
|
data: readonly(data),
|
||||||
|
openDialog,
|
||||||
|
confirm,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SettingsDialog from '~/components/Settings/Dialog.vue';
|
import Dialog from '~/components/Dialog/index.vue';
|
||||||
|
|
||||||
const { colorScheme } = await useUserSettings();
|
const { colorScheme } = await useUserSettings();
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ useKeyboardShortcuts();
|
|||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<SettingsDialog />
|
<ClientOnly>
|
||||||
|
<Dialog />
|
||||||
<Dropdown />
|
<Dropdown />
|
||||||
|
</ClientOnly>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user