feat: make navigation use native links (#973)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: RouteLocationRaw
|
||||
active?: boolean
|
||||
exact?: boolean
|
||||
title?: string
|
||||
}>(), {
|
||||
active: undefined,
|
||||
exact: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterLink v-slot="slotProps" :to="props.to" custom>
|
||||
<a
|
||||
class="route-link-item"
|
||||
:class="{ active: props.active ?? (props.exact ? !!slotProps?.isExactActive : !!slotProps?.isActive) }"
|
||||
:href="slotProps?.href || '#'"
|
||||
:title="props.title"
|
||||
:aria-current="(props.active ?? (props.exact ? !!slotProps?.isExactActive : !!slotProps?.isActive)) ? 'page' : undefined"
|
||||
@click="slotProps?.navigate"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
</RouterLink>
|
||||
</template>
|
||||
@@ -60,6 +60,20 @@ const showSessions = ref(
|
||||
let mobileQuery: MediaQueryList | null = null;
|
||||
const isMobile = ref(false);
|
||||
|
||||
function sessionHref(sessionId: string) {
|
||||
const profile = sessionProfile(sessionId);
|
||||
return router.resolve({
|
||||
name: "hermes.session",
|
||||
params: { sessionId },
|
||||
query: profile ? { profile } : undefined,
|
||||
}).href;
|
||||
}
|
||||
|
||||
function openSessionInNewTab(sessionId: string) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.open(sessionHref(sessionId), "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
async function handleSessionClick(sessionId: string) {
|
||||
const session = chatStore.sessions.find((item) => item.id === sessionId);
|
||||
await router.push({
|
||||
@@ -442,6 +456,7 @@ const contextMenuOptions = computed(() => {
|
||||
},
|
||||
],
|
||||
})
|
||||
options.push({ label: t("chat.openSessionInNewTab"), key: "open-link" })
|
||||
options.push({ label: t("chat.copySessionLink"), key: "copy-link" })
|
||||
options.push({ label: t("chat.copySessionId"), key: "copy-id" })
|
||||
return options
|
||||
@@ -478,6 +493,8 @@ async function handleContextMenuSelect(key: string) {
|
||||
copySessionLink(contextSessionId.value);
|
||||
} else if (key === "copy-id") {
|
||||
copySessionId(contextSessionId.value);
|
||||
} else if (key === "open-link") {
|
||||
openSessionInNewTab(contextSessionId.value);
|
||||
} else if (parseExportKey(key)) {
|
||||
const { mode, ext } = parseExportKey(key)!;
|
||||
const loadingMsg = mode === "compressed" ? message.loading(t("chat.exportCompressing"), { duration: 0 }) : null;
|
||||
@@ -846,6 +863,7 @@ async function handleSessionModelCustomSubmit() {
|
||||
:selectable="isBatchMode"
|
||||
:selected="isSessionSelected(s.id)"
|
||||
:show-profile="true"
|
||||
:to="sessionHref(s.id)"
|
||||
@select="handleSessionClick(s.id)"
|
||||
@contextmenu="handleContextMenu($event, s.id)"
|
||||
@delete="handleDeleteSession(s.id)"
|
||||
@@ -867,6 +885,7 @@ async function handleSessionModelCustomSubmit() {
|
||||
:selectable="isBatchMode"
|
||||
:selected="isSessionSelected(s.id)"
|
||||
:show-profile="true"
|
||||
:to="sessionHref(s.id)"
|
||||
@select="handleSessionClick(s.id)"
|
||||
@contextmenu="handleContextMenu($event, s.id)"
|
||||
@delete="handleDeleteSession(s.id)"
|
||||
@@ -1714,6 +1733,7 @@ async function handleSessionModelCustomSubmit() {
|
||||
border-radius: $radius-sm;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
color: $text-secondary;
|
||||
transition: all $transition-fast;
|
||||
margin-bottom: 2px;
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = withDefaults(defineProps<{
|
||||
selectable?: boolean
|
||||
selected?: boolean
|
||||
showProfile?: boolean
|
||||
to?: string
|
||||
}>(), {
|
||||
showProfile: true,
|
||||
})
|
||||
@@ -77,11 +78,18 @@ function onTouchMove() {
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
function isModifiedNavigation(event?: MouseEvent) {
|
||||
return !!event && (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0)
|
||||
}
|
||||
|
||||
function onClick(event?: MouseEvent) {
|
||||
if (longPressTriggered.value) {
|
||||
longPressTriggered.value = false
|
||||
event?.preventDefault()
|
||||
return
|
||||
}
|
||||
if (isModifiedNavigation(event)) return
|
||||
if (props.to && !props.selectable) event?.preventDefault()
|
||||
emit('select')
|
||||
}
|
||||
|
||||
@@ -91,10 +99,13 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
<component
|
||||
:is="selectable || !to ? 'button' : 'a'"
|
||||
class="session-item"
|
||||
:class="{ active, 'batch-mode': selectable, 'missing-models': profileModelsMissing }"
|
||||
:aria-current="active ? 'page' : undefined"
|
||||
:href="!selectable ? to : undefined"
|
||||
:type="selectable || !to ? 'button' : undefined"
|
||||
@click="onClick"
|
||||
@contextmenu="emit('contextmenu', $event)"
|
||||
@touchstart="onTouchStart"
|
||||
@@ -119,7 +130,7 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<NTooltip v-if="profileModelsMissing" trigger="click" placement="top">
|
||||
<template #trigger>
|
||||
<button class="session-item-warning" type="button" @click.stop>
|
||||
<button class="session-item-warning" type="button" @click.stop.prevent>
|
||||
!
|
||||
</button>
|
||||
</template>
|
||||
@@ -137,13 +148,13 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<NPopconfirm v-if="canDelete && !selectable" @positive-click="emit('delete')">
|
||||
<template #trigger>
|
||||
<button class="session-item-delete" @click.stop>
|
||||
<button class="session-item-delete" @click.stop.prevent>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</template>
|
||||
{{ t('chat.deleteSession') }}
|
||||
</NPopconfirm>
|
||||
</button>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { NButton, NModal, useMessage } from "naive-ui";
|
||||
@@ -9,6 +9,8 @@ import ProfileSelector from "./ProfileSelector.vue";
|
||||
import LanguageSwitch from "./LanguageSwitch.vue";
|
||||
import ThemeSwitch from "./ThemeSwitch.vue";
|
||||
import { useSessionSearch } from '@/composables/useSessionSearch'
|
||||
import { usePersistentRecord } from '@/composables/usePersistentRecord'
|
||||
import RouteLinkItem from '@/components/common/RouteLinkItem.vue'
|
||||
import { changelog } from "@/data/changelog";
|
||||
import { isStoredSuperAdmin } from "@/api/client";
|
||||
|
||||
@@ -25,9 +27,13 @@ const selectedKey = computed(() => {
|
||||
return route.name as string;
|
||||
});
|
||||
const isSuperAdmin = computed(() => isStoredSuperAdmin());
|
||||
|
||||
function isNavActive(...names: string[]) {
|
||||
return names.includes(selectedKey.value);
|
||||
}
|
||||
const logoPath = '/logo.png';
|
||||
|
||||
const collapsedGroups = reactive<Record<string, boolean>>({});
|
||||
const { record: collapsedGroups, persist: persistCollapsedGroups } = usePersistentRecord('hermes.sidebar.collapsedGroups');
|
||||
|
||||
type SidebarGroupKey = "Conversation" | "Agent" | "Monitoring" | "System";
|
||||
|
||||
@@ -37,15 +43,13 @@ function groupLabel(key: SidebarGroupKey) {
|
||||
|
||||
function toggleGroup(key: string) {
|
||||
collapsedGroups[key] = !collapsedGroups[key];
|
||||
persistCollapsedGroups();
|
||||
}
|
||||
|
||||
function isGroupCollapsed(key: string) {
|
||||
return !!collapsedGroups[key];
|
||||
}
|
||||
|
||||
function handleNav(key: string) {
|
||||
router.push({ name: key });
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
const ok = await appStore.doUpdate();
|
||||
@@ -75,11 +79,11 @@ function openChangelog() {
|
||||
|
||||
<template>
|
||||
<aside class="sidebar" :class="{ open: appStore.sidebarOpen, collapsed: appStore.sidebarCollapsed }">
|
||||
<div class="sidebar-logo" @click="router.push('/hermes/chat')">
|
||||
<RouteLinkItem class="sidebar-logo" :to="{ name: 'hermes.chat' }">
|
||||
<img :src="logoPath" alt="Hermes" class="logo-img" />
|
||||
<span class="logo-text">Hermes</span>
|
||||
<!-- <video class="logo-dance" :src="isDark ? danceVideoDark : danceVideoLight" autoplay loop muted playsinline /> -->
|
||||
</div>
|
||||
</RouteLinkItem>
|
||||
|
||||
<button class="collapse-btn" @click="appStore.toggleSidebarCollapsed()" :title="appStore.sidebarCollapsed ? t('sidebar.expand') : t('sidebar.collapse')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -98,20 +102,20 @@ function openChangelog() {
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="!isGroupCollapsed('conversation')" class="nav-group-items">
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.chat' }" @click="handleNav('hermes.chat')">
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.chat' }" :active="isNavActive('hermes.chat', 'hermes.session')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.chat") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.history' }" @click="handleNav('hermes.history')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.history' }" :active="isNavActive('hermes.history', 'hermes.historySession')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.history") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.groupChat' }" @click="handleNav('hermes.groupChat')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.groupChat' }" :active="isNavActive('hermes.groupChat', 'hermes.groupChatRoom')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
@@ -119,7 +123,7 @@ function openChangelog() {
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.groupChat") }}<span class="beta-tag">(beta)</span></span>
|
||||
</button>
|
||||
</RouteLinkItem>
|
||||
<button class="nav-item" @click="openSessionSearch">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
@@ -143,7 +147,7 @@ function openChangelog() {
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="!isGroupCollapsed('agent')" class="nav-group-items">
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.jobs' }" @click="handleNav('hermes.jobs')">
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.jobs' }" :active="selectedKey === 'hermes.jobs'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
|
||||
<line x1="16" y1="2" x2="16" y2="6" />
|
||||
@@ -151,45 +155,45 @@ function openChangelog() {
|
||||
<line x1="3" y1="10" x2="21" y2="10" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.jobs") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.kanban' }" @click="handleNav('hermes.kanban')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.kanban' }" :active="selectedKey === 'hermes.kanban'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="5" height="18" rx="1" />
|
||||
<rect x="10" y="3" width="5" height="12" rx="1" />
|
||||
<rect x="17" y="3" width="5" height="18" rx="1" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.kanban") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.channels' }" @click="handleNav('hermes.channels')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.channels' }" :active="selectedKey === 'hermes.channels'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.channels") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.skills' }" @click="handleNav('hermes.skills')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.skills' }" :active="selectedKey === 'hermes.skills'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2" />
|
||||
<polyline points="2 17 12 22 22 17" />
|
||||
<polyline points="2 12 12 17 22 12" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.skills") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.plugins' }" @click="handleNav('hermes.plugins')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.plugins' }" :active="selectedKey === 'hermes.plugins'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l2.1-2.1a4 4 0 0 1-5.3 5.3l-7.8 7.8a2.1 2.1 0 0 1-3-3l7.8-7.8a4 4 0 0 1 5.3-5.3l-2.1 2.1z" />
|
||||
<path d="M5 19l1-1" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.plugins") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.memory' }" @click="handleNav('hermes.memory')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.memory' }" :active="selectedKey === 'hermes.memory'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18h6" />
|
||||
<path d="M10 22h4" />
|
||||
<path d="M12 2a7 7 0 0 0-4 12.7V17h8v-2.3A7 7 0 0 0 12 2z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.memory") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.models' }" @click="handleNav('hermes.models')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.models' }" :active="selectedKey === 'hermes.models'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M12 1v4" />
|
||||
@@ -202,7 +206,7 @@ function openChangelog() {
|
||||
<path d="M16.95 7.05l2.83-2.83" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.models") }}</span>
|
||||
</button>
|
||||
</RouteLinkItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +219,7 @@ function openChangelog() {
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="!isGroupCollapsed('monitoring')" class="nav-group-items">
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.logs' }" @click="handleNav('hermes.logs')">
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.logs' }" :active="selectedKey === 'hermes.logs'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
@@ -224,28 +228,28 @@ function openChangelog() {
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.logs") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.usage' }" @click="handleNav('hermes.usage')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.usage' }" :active="selectedKey === 'hermes.usage'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="12" width="4" height="9" rx="1" />
|
||||
<rect x="10" y="7" width="4" height="14" rx="1" />
|
||||
<rect x="17" y="3" width="4" height="18" rx="1" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.usage") }}</span>
|
||||
</button>
|
||||
<button v-if="isSuperAdmin" class="nav-item" :class="{ active: selectedKey === 'hermes.performance' }" @click="handleNav('hermes.performance')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem v-if="isSuperAdmin" class="nav-item" :to="{ name: 'hermes.performance' }" :active="selectedKey === 'hermes.performance'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.performance") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.skillsUsage' }" @click="handleNav('hermes.skillsUsage')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.skillsUsage' }" :active="selectedKey === 'hermes.skillsUsage'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21.21 15.89A10 10 0 1 1 8.11 2.79" />
|
||||
<path d="M22 12A10 10 0 0 0 12 2v10z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.skillsUsage") }}</span>
|
||||
</button>
|
||||
</RouteLinkItem>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,20 +262,20 @@ function openChangelog() {
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="!isGroupCollapsed('system')" class="nav-group-items">
|
||||
<button v-if="isSuperAdmin" class="nav-item" :class="{ active: selectedKey === 'hermes.profiles' }" @click="handleNav('hermes.profiles')">
|
||||
<RouteLinkItem v-if="isSuperAdmin" class="nav-item" :to="{ name: 'hermes.profiles' }" :active="selectedKey === 'hermes.profiles'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.profiles") }}</span>
|
||||
</button>
|
||||
<button class="nav-item" :class="{ active: selectedKey === 'hermes.settings' }" @click="handleNav('hermes.settings')">
|
||||
</RouteLinkItem>
|
||||
<RouteLinkItem class="nav-item" :to="{ name: 'hermes.settings' }" :active="selectedKey === 'hermes.settings'">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
<span>{{ t("sidebar.settings") }}</span>
|
||||
</button>
|
||||
</RouteLinkItem>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -480,6 +484,8 @@ function openChangelog() {
|
||||
padding: 12px;
|
||||
border: none;
|
||||
background: none;
|
||||
appearance: none;
|
||||
text-decoration: none;
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
border-radius: $radius-sm;
|
||||
@@ -801,6 +807,8 @@ function openChangelog() {
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: none;
|
||||
appearance: none;
|
||||
text-decoration: none;
|
||||
color: $text-muted;
|
||||
border-radius: $radius-sm;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export function usePersistentRecord(key: string) {
|
||||
let initial: Record<string, boolean> = {}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key)
|
||||
if (raw) initial = JSON.parse(raw)
|
||||
} catch {
|
||||
initial = {}
|
||||
}
|
||||
}
|
||||
|
||||
const record = reactive<Record<string, boolean>>({ ...initial })
|
||||
|
||||
function persist() {
|
||||
if (typeof window === 'undefined') return
|
||||
window.localStorage.setItem(key, JSON.stringify({ ...record }))
|
||||
}
|
||||
|
||||
return { record, persist }
|
||||
}
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: 'Benutzer',
|
||||
monitorRoleAssistant: 'Assistent',
|
||||
copySessionLink: 'Sitzungslink kopieren',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: 'Sitzungs-ID kopieren',
|
||||
export: 'Exportieren',
|
||||
exportFull: 'Vollständiger Export (JSON)',
|
||||
|
||||
@@ -289,6 +289,8 @@ export default {
|
||||
monitorRoleUser: 'User',
|
||||
monitorRoleAssistant: 'Assistant',
|
||||
copySessionLink: 'Copy Session Link',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: 'Copy Session ID',
|
||||
export: 'Export',
|
||||
exportFull: 'Full Export (JSON)',
|
||||
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: 'Usuario',
|
||||
monitorRoleAssistant: 'Asistente',
|
||||
copySessionLink: 'Copiar enlace de sesión',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: 'Copiar ID de sesión',
|
||||
export: 'Exportar',
|
||||
exportFull: 'Exportación completa (JSON)',
|
||||
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: 'Utilisateur',
|
||||
monitorRoleAssistant: 'Assistant',
|
||||
copySessionLink: 'Copier le lien de session',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: "Copier l'ID de session",
|
||||
export: 'Exporter',
|
||||
exportFull: 'Export complet (JSON)',
|
||||
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: 'ユーザー',
|
||||
monitorRoleAssistant: 'アシスタント',
|
||||
copySessionLink: 'セッションリンクをコピー',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: 'セッション ID をコピー',
|
||||
export: 'エクスポート',
|
||||
exportFull: 'フルエクスポート (JSON)',
|
||||
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: '사용자',
|
||||
monitorRoleAssistant: '어시스턴트',
|
||||
copySessionLink: '세션 링크 복사',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: '세션 ID 복사',
|
||||
export: '내보내기',
|
||||
exportFull: '전체 내보내기 (JSON)',
|
||||
|
||||
@@ -273,6 +273,8 @@ export default {
|
||||
monitorRoleUser: 'Usuário',
|
||||
monitorRoleAssistant: 'Assistente',
|
||||
copySessionLink: 'Copiar link da sessão',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: 'Copiar ID da sessão',
|
||||
export: 'Exportar',
|
||||
exportFull: 'Exportação completa (JSON)',
|
||||
|
||||
@@ -287,6 +287,8 @@ export default {
|
||||
monitorRoleUser: '使用者',
|
||||
monitorRoleAssistant: '助手',
|
||||
copySessionLink: '複製工作階段連結',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: '複製工作階段 ID',
|
||||
export: '匯出',
|
||||
exportFull: '完整匯出 (JSON)',
|
||||
|
||||
@@ -289,6 +289,8 @@ export default {
|
||||
monitorRoleUser: '用户',
|
||||
monitorRoleAssistant: '助手',
|
||||
copySessionLink: '复制会话链接',
|
||||
openSessionInNewTab: 'Open in new tab',
|
||||
sessionLinkCopied: 'Session link copied',
|
||||
copySessionId: '复制会话 ID',
|
||||
export: '导出',
|
||||
exportFull: '全量导出 (JSON)',
|
||||
|
||||
Reference in New Issue
Block a user