Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@
"RightPanel": {
"Open": "Open right panel",
"Close": "Close right panel",
"AIOpen": "Open AI assistant",
"AIClose": "Close AI assistant",
"Session": "Session",
"Control": "Control",
"Files": "Files",
Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@
"RightPanel": {
"Open": "打开右侧面板",
"Close": "关闭右侧面板",
"AIOpen": "打开 AI 助手",
"AIClose": "关闭 AI 助手",
"Session": "会话",
"Control": "控制",
"Files": "文件",
Expand Down
4 changes: 2 additions & 2 deletions ui/chen/workspaces/DatabaseSessionSurface.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const emit = defineEmits<{ reconnect: [] }>();

const toast = useToast();
const { locale, t } = useI18n();
const { openWithTab } = useRightPanel();
const { openAi } = useAiPanel();
const { addErrorToast } = useErrorToast();
const userInfoStore = useUserInfoStore();
const { markSessionConnected, markSessionFailed } = useWorkspaceTabs();
Expand Down Expand Up @@ -597,7 +597,7 @@ function connectSqlAiSession() {
}

function openSqlAi() {
openWithTab("ai");
openAi();
}

function requestSqlAi(operation: ChenSqlAiOperation) {
Expand Down
123 changes: 24 additions & 99 deletions ui/components/Header/ActionButtons.vue
Original file line number Diff line number Diff line change
@@ -1,120 +1,45 @@
<script setup lang="ts">
import type { DropdownMenuItem } from "@nuxt/ui";
import type { LangType } from "~/types";

import Profile from "~/components/SideBar/profile.vue";

const { t, locales, locale } = useI18n();
const { openSettings } = useSettingsWindow();
const { themeDropdownItems } = useThemeOptions();
const { setLang } = useSettingManager();
const localePath = useLocalePath();
const { t } = useI18n();
const { activeWorkspaceMode } = useWorkspaceMode();
const { activeTab: rightPanelTab, open: rightPanelOpen, toggle: toggleRightPanel } = useRightPanel();
const { open: aiPanelOpen, toggleAi } = useAiPanel();
const showRightPanelButton = computed(() => activeWorkspaceMode.value !== "files");
const aiButtonLabel = computed(() => t(aiPanelOpen.value ? "RightPanel.AIClose" : "RightPanel.AIOpen"));

const commonButtonProps = {
size: "sm" as const,
color: "neutral" as const
const handleToggleAi = () => {
toggleAi(rightPanelOpen.value && rightPanelTab.value === "sftp" ? "sftp" : "workspace");
};

const { open: rightPanelOpen, toggle: toggleRightPanel } = useRightPanel();

const languageItems = computed<DropdownMenuItem[]>(() =>
((locales.value as Array<{ code?: string; name?: string } | string>) || []).map((item) => {
const code = typeof item === "string" ? item : item.code || "";
const label = typeof item === "string" ? item : item.name || code;

return {
label,
type: "checkbox",
checked: locale.value === code,
onUpdateChecked: (checked: boolean) => {
if (checked && code) setLang(code as LangType);
}
};
})
);

const toolItems = computed<DropdownMenuItem[]>(() => [
{
label: t("Menu.Player"),
icon: "lucide:clapperboard",
to: localePath("videoplayer")
},
{
label: t("Menu.Transcode"),
icon: "lucide:repeat-2",
to: localePath({ path: "/transcode" })
}
]);

const appMenuItems = computed<DropdownMenuItem[]>(() => [
{
label: t("Common.Language"),
icon: "i-lucide-languages",
children: languageItems.value
},
...(isDesktopRuntime()
? [
{
label: t("Menu.MyTools"),
icon: "i-lucide-wrench",
children: toolItems.value
} satisfies DropdownMenuItem
]
: []),
{
label: t("Common.SwitchTheme"),
icon: "i-lucide-palette",
children: themeDropdownItems.value
},
{ type: "separator" },
{
label: t("Common.Preferences"),
icon: "i-lucide-settings",
onSelect: () => void openSettings()
},
{
label: t("DesktopMenu.About"),
icon: "i-lucide-info",
onSelect: () => void openSettings("/setting/about")
}
]);
</script>

<template>
<section class="flex items-center h-full">
<div class="flex items-center gap-1 px-2">
<UDropdownMenu
:items="appMenuItems"
size="sm"
:content="{ align: 'end', side: 'bottom', sideOffset: 6 }"
:ui="{
item: '!items-center leading-none',
itemLeadingIcon: 'size-4 shrink-0 block leading-none',
itemTrailingIcon: '!size-4 shrink-0 block leading-none',
itemLabel: 'block truncate text-start leading-none',
itemWrapper: 'min-w-0 flex-1 justify-start text-start'
}"
>
<UTooltip arrow :text="t('Common.AppMenu')">
<UButton
icon="i-lucide-settings"
:aria-label="t('Common.AppMenu')"
v-bind="commonButtonProps"
variant="ghost"
:ui="{ leadingIcon: 'size-4' }"
/>
</UTooltip>
</UDropdownMenu>
<UTooltip arrow :text="aiButtonLabel">
<UButton
icon="i-lucide-sparkles"
:aria-label="aiButtonLabel"
:aria-pressed="aiPanelOpen"
size="sm"
color="primary"
:variant="aiPanelOpen ? 'soft' : 'ghost'"
:ui="{ leadingIcon: 'size-4' }"
@click="handleToggleAi"
/>
</UTooltip>

<Profile placement="topbar" />

<UTooltip arrow :text="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')">
<UTooltip v-if="showRightPanelButton" arrow :text="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')">
<UButton
:icon="rightPanelOpen ? 'i-lucide-panel-right-close' : 'i-lucide-panel-right'"
:aria-label="rightPanelOpen ? t('RightPanel.Close') : t('RightPanel.Open')"
:aria-pressed="rightPanelOpen"
v-bind="commonButtonProps"
size="sm"
color="neutral"
:variant="rightPanelOpen ? 'soft' : 'ghost'"
:ui="{ leadingIcon: 'size-4' }"
@click="toggleRightPanel"
/>
</UTooltip>
Expand Down
38 changes: 38 additions & 0 deletions ui/components/RightPanel/AiOverlayPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
const emit = defineEmits<{ close: [] }>();
const { t } = useI18n();
const isNarrowScreen = useMediaQuery("(max-width: 767px)");
</script>

<template>
<div class="pointer-events-none absolute inset-0 z-50">
<button
v-if="isNarrowScreen"
type="button"
class="pointer-events-auto absolute inset-0 bg-black/35 backdrop-blur-[1px]"
:aria-label="t('RightPanel.AIClose')"
@click="emit('close')"
/>

<aside
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"
>
<div class="flex h-10 shrink-0 items-center gap-2 border-b border-[var(--app-border)] px-3">
<UIcon name="i-lucide-sparkles" class="size-4 text-primary" />
<span class="min-w-0 flex-1 truncate text-sm font-semibold">{{ t("RightPanel.AI") }}</span>
<UButton
icon="i-lucide-x"
:aria-label="t('RightPanel.AIClose')"
color="neutral"
variant="ghost"
size="xs"
@click="emit('close')"
/>
</div>

<div class="min-h-0 flex-1 overflow-hidden">
<RightPanelAiPanel />
</div>
</aside>
</div>
</template>
4 changes: 2 additions & 2 deletions ui/components/RightPanel/aiPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useAiPanelController } from "./ai/useAiPanelController";

const { activePaneId, activeTab } = useWorkspaceTabs();
const { activeWorkspaceMode } = useWorkspaceMode();
const { aiSourceTab } = useRightPanel();
const { source: aiSource } = useAiPanel();
const { t } = useI18n();
const activeSurface = computed(() => {
if (activeWorkspaceMode.value === "files") return null;
Expand All @@ -31,7 +31,7 @@ const globalFileTargetId = computed(() => getActiveKokoFileAiTargetId(KOKO_GLOBA
const compactFileOwnerId = computed(() => createKokoCompactFileAiOwnerId(activeSurface.value?.id || ""));
const compactFileTargetId = computed(() => getActiveKokoFileAiTargetId(compactFileOwnerId.value) || "");
const preferCompactFileAi = computed(
() => aiSourceTab.value === "sftp" && activeSurface.value?.protocol?.toLowerCase() === "ssh"
() => aiSource.value === "sftp" && activeSurface.value?.protocol?.toLowerCase() === "ssh"
);
const aiTargetId = computed(() =>
resolveAiPanelTarget({
Expand Down
15 changes: 7 additions & 8 deletions ui/components/RightPanel/rightPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getLionWorkspaceSession } from "@/lion/workspaces/useLionWorkspaceSessi
const { t } = useI18n();
const { activePaneId, activeTab: workspaceTab } = useWorkspaceTabs();
const { activeWorkspaceMode } = useWorkspaceMode();
const { activeTab, setActiveTab } = useRightPanel();
const { activeTab, setActiveTab, setOpen } = useRightPanel();
const activeSession = computed(() => {
if (activeWorkspaceMode.value === "files") return null;
const tab = workspaceTab.value;
Expand All @@ -18,12 +18,10 @@ const lionSession = computed(() => getLionWorkspaceSession(activeSession.value?.
const showSftpTab = computed(() => activeSession.value?.protocol?.toLowerCase() === "ssh");

const tabs = computed(() => {
if (activeWorkspaceMode.value === "files") {
return [{ value: "ai" as const, label: t("RightPanel.AI"), icon: "i-lucide-sparkles" }];
}
if (activeWorkspaceMode.value === "files") return [];

const items: Array<{ value: RightPanelTab; label: string; icon: string; title?: string }> = [
{ value: "session" as const, label: t("RightPanel.Session"), icon: "i-lucide-terminal" }
{ value: "session", label: t("RightPanel.Session"), icon: "i-lucide-terminal" }
];

if (lionSession.value) {
Expand All @@ -50,16 +48,13 @@ const tabs = computed(() => {
});
}

items.push({ value: "ai" as const, label: t("RightPanel.AI"), icon: "i-lucide-sparkles" });

return items;
});

const panelComponents = {
session: defineAsyncComponent(() => import("~/components/RightPanel/sessionPanel.vue")),
"lion-control": defineAsyncComponent(() => import("~/components/RightPanel/lionControlPanel.vue")),
"lion-files": defineAsyncComponent(() => import("~/components/RightPanel/lionFilePanel.vue")),
ai: defineAsyncComponent(() => import("~/components/RightPanel/aiPanel.vue")),
sftp: defineAsyncComponent(() => import("~/components/RightPanel/sftpPanel.vue"))
} as const;

Expand All @@ -68,6 +63,10 @@ const activePanelComponent = computed(() => panelComponents[activeTab.value]);
watch(
tabs,
(items) => {
if (!items.length) {
setOpen(false);
return;
}
if (!items.some((item) => item.value === activeTab.value)) setActiveTab(items[0]?.value || "session");
},
{ immediate: true }
Expand Down
Loading
Loading