Skip to content

Commit 7b99ea0

Browse files
committed
feat(ui): prioritize AI assistant and simplify account menu
1 parent 90c72ce commit 7b99ea0

17 files changed

Lines changed: 399 additions & 199 deletions

File tree

‎i18n/locales/en.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@
247247
"RightPanel": {
248248
"Open": "Open right panel",
249249
"Close": "Close right panel",
250+
"AIOpen": "Open AI assistant",
251+
"AIClose": "Close AI assistant",
250252
"Session": "Session",
251253
"Control": "Control",
252254
"Files": "Files",

‎i18n/locales/zh.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@
247247
"RightPanel": {
248248
"Open": "打开右侧面板",
249249
"Close": "关闭右侧面板",
250+
"AIOpen": "打开 AI 助手",
251+
"AIClose": "关闭 AI 助手",
250252
"Session": "会话",
251253
"Control": "控制",
252254
"Files": "文件",

‎ui/chen/workspaces/DatabaseSessionSurface.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const emit = defineEmits<{ reconnect: [] }>();
9292
9393
const toast = useToast();
9494
const { locale, t } = useI18n();
95-
const { openWithTab } = useRightPanel();
95+
const { openAi } = useAiPanel();
9696
const { addErrorToast } = useErrorToast();
9797
const userInfoStore = useUserInfoStore();
9898
const { markSessionConnected, markSessionFailed } = useWorkspaceTabs();
@@ -597,7 +597,7 @@ function connectSqlAiSession() {
597597
}
598598
599599
function openSqlAi() {
600-
openWithTab("ai");
600+
openAi();
601601
}
602602
603603
function requestSqlAi(operation: ChenSqlAiOperation) {

‎ui/components/Header/ActionButtons.vue‎

Lines changed: 24 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,45 @@
11
<script setup lang="ts">
2-
import type { DropdownMenuItem } from "@nuxt/ui";
3-
import type { LangType } from "~/types";
4-
52
import Profile from "~/components/SideBar/profile.vue";
63
7-
const { t, locales, locale } = useI18n();
8-
const { openSettings } = useSettingsWindow();
9-
const { themeDropdownItems } = useThemeOptions();
10-
const { setLang } = useSettingManager();
11-
const localePath = useLocalePath();
4+
const { t } = useI18n();
5+
const { activeWorkspaceMode } = useWorkspaceMode();
6+
const { activeTab: rightPanelTab, open: rightPanelOpen, toggle: toggleRightPanel } = useRightPanel();
7+
const { open: aiPanelOpen, toggleAi } = useAiPanel();
8+
const showRightPanelButton = computed(() => activeWorkspaceMode.value !== "files");
9+
const aiButtonLabel = computed(() => t(aiPanelOpen.value ? "RightPanel.AIClose" : "RightPanel.AIOpen"));
1210
13-
const commonButtonProps = {
14-
size: "sm" as const,
15-
color: "neutral" as const
11+
const handleToggleAi = () => {
12+
toggleAi(rightPanelOpen.value && rightPanelTab.value === "sftp" ? "sftp" : "workspace");
1613
};
17-
18-
const { open: rightPanelOpen, toggle: toggleRightPanel } = useRightPanel();
19-
20-
const languageItems = computed<DropdownMenuItem[]>(() =>
21-
((locales.value as Array<{ code?: string; name?: string } | string>) || []).map((item) => {
22-
const code = typeof item === "string" ? item : item.code || "";
23-
const label = typeof item === "string" ? item : item.name || code;
24-
25-
return {
26-
label,
27-
type: "checkbox",
28-
checked: locale.value === code,
29-
onUpdateChecked: (checked: boolean) => {
30-
if (checked && code) setLang(code as LangType);
31-
}
32-
};
33-
})
34-
);
35-
36-
const toolItems = computed<DropdownMenuItem[]>(() => [
37-
{
38-
label: t("Menu.Player"),
39-
icon: "lucide:clapperboard",
40-
to: localePath("videoplayer")
41-
},
42-
{
43-
label: t("Menu.Transcode"),
44-
icon: "lucide:repeat-2",
45-
to: localePath({ path: "/transcode" })
46-
}
47-
]);
48-
49-
const appMenuItems = computed<DropdownMenuItem[]>(() => [
50-
{
51-
label: t("Common.Language"),
52-
icon: "i-lucide-languages",
53-
children: languageItems.value
54-
},
55-
...(isDesktopRuntime()
56-
? [
57-
{
58-
label: t("Menu.MyTools"),
59-
icon: "i-lucide-wrench",
60-
children: toolItems.value
61-
} satisfies DropdownMenuItem
62-
]
63-
: []),
64-
{
65-
label: t("Common.SwitchTheme"),
66-
icon: "i-lucide-palette",
67-
children: themeDropdownItems.value
68-
},
69-
{ type: "separator" },
70-
{
71-
label: t("Common.Preferences"),
72-
icon: "i-lucide-settings",
73-
onSelect: () => void openSettings()
74-
},
75-
{
76-
label: t("DesktopMenu.About"),
77-
icon: "i-lucide-info",
78-
onSelect: () => void openSettings("/setting/about")
79-
}
80-
]);
8114
</script>
8215

8316
<template>
8417
<section class="flex items-center h-full">
8518
<div class="flex items-center gap-1 px-2">
86-
<UDropdownMenu
87-
:items="appMenuItems"
88-
size="sm"
89-
:content="{ align: 'end', side: 'bottom', sideOffset: 6 }"
90-
:ui="{
91-
item: '!items-center leading-none',
92-
itemLeadingIcon: 'size-4 shrink-0 block leading-none',
93-
itemTrailingIcon: '!size-4 shrink-0 block leading-none',
94-
itemLabel: 'block truncate text-start leading-none',
95-
itemWrapper: 'min-w-0 flex-1 justify-start text-start'
96-
}"
97-
>
98-
<UTooltip arrow :text="t('Common.AppMenu')">
99-
<UButton
100-
icon="i-lucide-settings"
101-
:aria-label="t('Common.AppMenu')"
102-
v-bind="commonButtonProps"
103-
variant="ghost"
104-
:ui="{ leadingIcon: 'size-4' }"
105-
/>
106-
</UTooltip>
107-
</UDropdownMenu>
19+
<UTooltip arrow :text="aiButtonLabel">
20+
<UButton
21+
icon="i-lucide-sparkles"
22+
:aria-label="aiButtonLabel"
23+
:aria-pressed="aiPanelOpen"
24+
size="sm"
25+
color="primary"
26+
:variant="aiPanelOpen ? 'soft' : 'ghost'"
27+
:ui="{ leadingIcon: 'size-4' }"
28+
@click="handleToggleAi"
29+
/>
30+
</UTooltip>
10831

10932
<Profile placement="topbar" />
11033

111-
<UTooltip arrow :text="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')">
34+
<UTooltip v-if="showRightPanelButton" arrow :text="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')">
11235
<UButton
11336
:icon="rightPanelOpen ? 'i-lucide-panel-right-close' : 'i-lucide-panel-right'"
11437
:aria-label="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')"
11538
:aria-pressed="rightPanelOpen"
116-
v-bind="commonButtonProps"
39+
size="sm"
40+
color="neutral"
11741
:variant="rightPanelOpen ? 'soft' : 'ghost'"
42+
:ui="{ leadingIcon: 'size-4' }"
11843
@click="toggleRightPanel"
11944
/>
12045
</UTooltip>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
const emit = defineEmits<{ close: [] }>();
3+
const { t } = useI18n();
4+
const isNarrowScreen = useMediaQuery("(max-width: 767px)");
5+
</script>
6+
7+
<template>
8+
<div class="pointer-events-none absolute inset-0 z-50">
9+
<button
10+
v-if="isNarrowScreen"
11+
type="button"
12+
class="pointer-events-auto absolute inset-0 bg-black/35 backdrop-blur-[1px]"
13+
:aria-label="t('RightPanel.AIClose')"
14+
@click="emit('close')"
15+
/>
16+
17+
<aside
18+
class="pointer-events-auto absolute inset-y-0 right-0 flex w-[min(380px,calc(100vw-3rem))] min-h-0 flex-col border-l border-[var(--app-border)] bg-[var(--app-panel-bg)] text-[var(--app-fg)] shadow-2xl"
19+
>
20+
<div class="flex h-10 shrink-0 items-center gap-2 border-b border-[var(--app-border)] px-3">
21+
<UIcon name="i-lucide-sparkles" class="size-4 text-primary" />
22+
<span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ t("RightPanel.AI") }}</span>
23+
<UButton
24+
icon="i-lucide-x"
25+
:aria-label="t('RightPanel.AIClose')"
26+
color="neutral"
27+
variant="ghost"
28+
size="xs"
29+
@click="emit('close')"
30+
/>
31+
</div>
32+
33+
<div class="min-h-0 flex-1 overflow-hidden">
34+
<RightPanelAiPanel />
35+
</div>
36+
</aside>
37+
</div>
38+
</template>

‎ui/components/RightPanel/aiPanel.vue‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useAiPanelController } from "./ai/useAiPanelController";
1313
1414
const { activePaneId, activeTab } = useWorkspaceTabs();
1515
const { activeWorkspaceMode } = useWorkspaceMode();
16-
const { aiSourceTab } = useRightPanel();
16+
const { source: aiSource } = useAiPanel();
1717
const { t } = useI18n();
1818
const activeSurface = computed(() => {
1919
if (activeWorkspaceMode.value === "files") return null;
@@ -31,7 +31,7 @@ const globalFileTargetId = computed(() => getActiveKokoFileAiTargetId(KOKO_GLOBA
3131
const compactFileOwnerId = computed(() => createKokoCompactFileAiOwnerId(activeSurface.value?.id || ""));
3232
const compactFileTargetId = computed(() => getActiveKokoFileAiTargetId(compactFileOwnerId.value) || "");
3333
const preferCompactFileAi = computed(
34-
() => aiSourceTab.value === "sftp" && activeSurface.value?.protocol?.toLowerCase() === "ssh"
34+
() => aiSource.value === "sftp" && activeSurface.value?.protocol?.toLowerCase() === "ssh"
3535
);
3636
const aiTargetId = computed(() =>
3737
resolveAiPanelTarget({

‎ui/components/RightPanel/rightPanel.vue‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getLionWorkspaceSession } from "@/lion/workspaces/useLionWorkspaceSessi
55
const { t } = useI18n();
66
const { activePaneId, activeTab: workspaceTab } = useWorkspaceTabs();
77
const { activeWorkspaceMode } = useWorkspaceMode();
8-
const { activeTab, setActiveTab } = useRightPanel();
8+
const { activeTab, setActiveTab, setOpen } = useRightPanel();
99
const activeSession = computed(() => {
1010
if (activeWorkspaceMode.value === "files") return null;
1111
const tab = workspaceTab.value;
@@ -18,12 +18,10 @@ const lionSession = computed(() => getLionWorkspaceSession(activeSession.value?.
1818
const showSftpTab = computed(() => activeSession.value?.protocol?.toLowerCase() === "ssh");
1919
2020
const tabs = computed(() => {
21-
if (activeWorkspaceMode.value === "files") {
22-
return [{ value: "ai" as const, label: t("RightPanel.AI"), icon: "i-lucide-sparkles" }];
23-
}
21+
if (activeWorkspaceMode.value === "files") return [];
2422
2523
const items: Array<{ value: RightPanelTab; label: string; icon: string; title?: string }> = [
26-
{ value: "session" as const, label: t("RightPanel.Session"), icon: "i-lucide-terminal" }
24+
{ value: "session", label: t("RightPanel.Session"), icon: "i-lucide-terminal" }
2725
];
2826
2927
if (lionSession.value) {
@@ -50,16 +48,13 @@ const tabs = computed(() => {
5048
});
5149
}
5250
53-
items.push({ value: "ai" as const, label: t("RightPanel.AI"), icon: "i-lucide-sparkles" });
54-
5551
return items;
5652
});
5753
5854
const panelComponents = {
5955
session: defineAsyncComponent(() => import("~/components/RightPanel/sessionPanel.vue")),
6056
"lion-control": defineAsyncComponent(() => import("~/components/RightPanel/lionControlPanel.vue")),
6157
"lion-files": defineAsyncComponent(() => import("~/components/RightPanel/lionFilePanel.vue")),
62-
ai: defineAsyncComponent(() => import("~/components/RightPanel/aiPanel.vue")),
6358
sftp: defineAsyncComponent(() => import("~/components/RightPanel/sftpPanel.vue"))
6459
} as const;
6560
@@ -68,6 +63,10 @@ const activePanelComponent = computed(() => panelComponents[activeTab.value]);
6863
watch(
6964
tabs,
7065
(items) => {
66+
if (!items.length) {
67+
setOpen(false);
68+
return;
69+
}
7170
if (!items.some((item) => item.value === activeTab.value)) setActiveTab(items[0]?.value || "session");
7271
},
7372
{ immediate: true }

0 commit comments

Comments
 (0)