UI: polish menus

This commit is contained in:
qixinbo
2026-03-29 15:24:08 +08:00
parent 2832ca91f3
commit 039f9e4b8b
3 changed files with 137 additions and 69 deletions
+131 -67
View File
@@ -1,7 +1,7 @@
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Settings, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe, Bot, Mic, Loader2, CheckCircle2, XCircle } from "lucide-react"; import { Menu, LayoutDashboard, Plus, MoreVertical, User, Search, Brain, Trash2, Pencil, Pin, Archive, Database, CheckSquare, Square, ListChecks, RotateCcw, Wand2, Folder, Globe, Bot, Loader2, CheckCircle2, XCircle, ChevronRight } from "lucide-react";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -377,6 +377,8 @@ function SidebarBody() {
const [showUserMenu, setShowUserMenu] = useState(false); const [showUserMenu, setShowUserMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const [voiceSettingsOpen, setVoiceSettingsOpen] = useState(false); const [voiceSettingsOpen, setVoiceSettingsOpen] = useState(false);
const [showKnowledgeSubmenu, setShowKnowledgeSubmenu] = useState(false);
const [showMoreSubmenu, setShowMoreSubmenu] = useState(false);
const [whisperUrlDraft, setWhisperUrlDraft] = useState(""); const [whisperUrlDraft, setWhisperUrlDraft] = useState("");
const [isTestingVoice, setIsTestingVoice] = useState(false); const [isTestingVoice, setIsTestingVoice] = useState(false);
const [voiceTestStatus, setVoiceTestStatus] = useState<"success" | "error" | null>(null); const [voiceTestStatus, setVoiceTestStatus] = useState<"success" | "error" | null>(null);
@@ -855,7 +857,13 @@ function SidebarBody() {
<div className="flex items-center justify-between text-muted-foreground"> <div className="flex items-center justify-between text-muted-foreground">
<button <button
className="flex items-center gap-2 hover:text-foreground transition-colors p-1 rounded-full hover:bg-muted" className="flex items-center gap-2 hover:text-foreground transition-colors p-1 rounded-full hover:bg-muted"
onClick={() => setShowUserMenu(!showUserMenu)} onClick={() => {
setShowUserMenu(!showUserMenu);
if (showUserMenu) {
setShowKnowledgeSubmenu(false);
setShowMoreSubmenu(false);
}
}}
> >
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm"> <div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600 border border-indigo-200 shadow-sm">
<User className="h-4.5 w-4.5" /> <User className="h-4.5 w-4.5" />
@@ -876,7 +884,7 @@ function SidebarBody() {
{/* User Settings Popover Menu */} {/* User Settings Popover Menu */}
{showUserMenu && ( {showUserMenu && (
<div className="absolute bottom-[72px] left-4 w-56 bg-background rounded-xl shadow-xl border border-border py-1.5 z-50 overflow-hidden animate-in fade-in zoom-in duration-200"> <div className="absolute bottom-[72px] left-4 w-56 bg-background rounded-xl shadow-xl border border-border py-1.5 z-50 overflow-visible animate-in fade-in zoom-in duration-200">
<div className="px-3 py-2 border-b border-border mb-1"> <div className="px-3 py-2 border-b border-border mb-1">
<p className="text-sm font-medium text-foreground truncate">{user?.username}</p> <p className="text-sm font-medium text-foreground truncate">{user?.username}</p>
<p className="text-xs text-muted-foreground truncate">{user?.email}</p> <p className="text-xs text-muted-foreground truncate">{user?.email}</p>
@@ -892,6 +900,18 @@ function SidebarBody() {
<Folder className="h-4 w-4 text-muted-foreground" /> <Folder className="h-4 w-4 text-muted-foreground" />
{t('projectManagement')} {t('projectManagement')}
</button> </button>
{user?.is_admin && (
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
onClick={() => {
navigate("/model-configs");
setShowUserMenu(false);
}}
>
<Brain className="h-4 w-4 text-muted-foreground" />
{t('modelConfig')}
</button>
)}
<button <button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
@@ -919,75 +939,119 @@ function SidebarBody() {
{t('dataSourceManagement')} {t('dataSourceManagement')}
</button> </button>
<button <div
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" className="relative group/kb"
onClick={() => { onMouseEnter={() => setShowKnowledgeSubmenu(true)}
navigate("/knowledge-bases"); onMouseLeave={() => setShowKnowledgeSubmenu(false)}
setShowUserMenu(false);
}}
> >
<Database className="h-4 w-4 text-muted-foreground" /> <button
{t('knowledgeBases')} className="w-full flex items-center justify-between px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
</button> onClick={(e) => {
e.preventDefault();
setShowKnowledgeSubmenu(!showKnowledgeSubmenu);
}}
>
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
{t('knowledgeBaseGroup', 'Knowledge Base')}
</div>
<ChevronRight className={`h-4 w-4 text-muted-foreground transition-transform ${showKnowledgeSubmenu ? 'translate-x-0.5' : ''}`} />
</button>
<button {showKnowledgeSubmenu && (
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" <div
onClick={() => { className="absolute left-[calc(100%-8px)] top-0 w-48 bg-background border border-border rounded-md shadow-lg py-1 z-[60] animate-in fade-in zoom-in-95 duration-200"
navigate("/settings"); style={{ minWidth: 'max-content' }}
setShowUserMenu(false); >
}} <div className="absolute -left-3 top-0 bottom-0 w-4" />
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors relative z-10"
onClick={() => {
navigate("/knowledge-bases");
setShowUserMenu(false);
setShowKnowledgeSubmenu(false);
}}
>
{t('knowledgeBases')}
</button>
{user?.is_admin && (
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors relative z-10"
onClick={() => {
navigate("/embedding-models");
setShowUserMenu(false);
setShowKnowledgeSubmenu(false);
}}
>
{t('embeddingModels')}
</button>
)}
</div>
)}
</div>
<div
className="relative"
onMouseEnter={() => setShowMoreSubmenu(true)}
onMouseLeave={() => setShowMoreSubmenu(false)}
> >
<Settings className="h-4 w-4 text-muted-foreground" /> <button
{t('personalSettings')} className="w-full flex items-center justify-between px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
</button> onClick={(e) => {
e.preventDefault();
setShowMoreSubmenu(!showMoreSubmenu);
}}
>
<div className="flex items-center gap-2">
<MoreVertical className="h-4 w-4 text-muted-foreground" />
{t('moreGroup')}
</div>
<ChevronRight className={`h-4 w-4 text-muted-foreground transition-transform ${showMoreSubmenu ? 'translate-x-0.5' : ''}`} />
</button>
<button {showMoreSubmenu && (
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" <div
onClick={() => { className="absolute left-[calc(100%-8px)] top-0 w-48 bg-background border border-border rounded-md shadow-lg py-1 z-[60] animate-in fade-in zoom-in-95 duration-200"
openVoiceSettings(); style={{ minWidth: 'max-content' }}
setShowUserMenu(false);
}}
>
<Mic className="h-4 w-4 text-muted-foreground" />
{t('voiceSettings')}
</button>
{user?.is_admin && (
<>
<button
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
onClick={() => {
navigate("/model-configs");
setShowUserMenu(false);
}}
> >
<Brain className="h-4 w-4 text-muted-foreground" /> <div className="absolute -left-3 top-0 bottom-0 w-4" />
{t('modelConfig')} <button
</button> className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors relative z-10"
onClick={() => {
<button navigate("/settings");
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" setShowUserMenu(false);
onClick={() => { setShowMoreSubmenu(false);
navigate("/embedding-models"); }}
setShowUserMenu(false); >
}} {t('personalSettings')}
> </button>
<Brain className="h-4 w-4 text-muted-foreground" /> {user?.is_admin && (
{t('embeddingModels')} <button
</button> className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors relative z-10"
onClick={() => {
<button navigate("/users");
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors" setShowUserMenu(false);
onClick={() => { setShowMoreSubmenu(false);
navigate("/users"); }}
setShowUserMenu(false); >
}} {t('userManagement')}
> </button>
<User className="h-4 w-4" /> )}
{t('userManagement')} <button
</button> className="w-full flex items-center gap-2 px-3 py-2 text-sm text-foreground/80 hover:bg-muted transition-colors relative z-10"
</> onClick={() => {
)} openVoiceSettings();
setShowUserMenu(false);
setShowMoreSubmenu(false);
}}
>
{t('voiceSettings')}
</button>
</div>
)}
</div>
<div className="h-px bg-muted my-1 mx-2" /> <div className="h-px bg-muted my-1 mx-2" />
+2
View File
@@ -365,6 +365,8 @@
"noSubagents": "No subagents configured", "noSubagents": "No subagents configured",
"confirmDeleteSubagent": "Are you sure you want to delete this subagent?", "confirmDeleteSubagent": "Are you sure you want to delete this subagent?",
"selectProjectToManageSubagents": "Please select a project to manage subagents", "selectProjectToManageSubagents": "Please select a project to manage subagents",
"knowledgeBaseGroup": "Knowledge Base",
"moreGroup": "More",
"knowledgeBases": "Knowledge Bases", "knowledgeBases": "Knowledge Bases",
"voiceSettings": "Voice Input Settings", "voiceSettings": "Voice Input Settings",
"embeddingModels": "Embedding Models" "embeddingModels": "Embedding Models"
+4 -2
View File
@@ -354,8 +354,8 @@
"noMcpServers": "暂无 MCP 服务器", "noMcpServers": "暂无 MCP 服务器",
"confirmDeleteMcpServer": "确定要删除这个 MCP 服务器吗?", "confirmDeleteMcpServer": "确定要删除这个 MCP 服务器吗?",
"saveMcpServer": "保存 MCP 服务器", "saveMcpServer": "保存 MCP 服务器",
"subagents": "智能体", "subagents": "智能体编排",
"subagentManagement": "智能体管理", "subagentManagement": "智能体编排",
"manageSubagentsDesc": "管理该项目的子智能体", "manageSubagentsDesc": "管理该项目的子智能体",
"addSubagent": "添加子智能体", "addSubagent": "添加子智能体",
"editSubagent": "编辑子智能体", "editSubagent": "编辑子智能体",
@@ -365,6 +365,8 @@
"noSubagents": "暂无配置的子智能体", "noSubagents": "暂无配置的子智能体",
"confirmDeleteSubagent": "确定要删除这个子智能体吗?", "confirmDeleteSubagent": "确定要删除这个子智能体吗?",
"selectProjectToManageSubagents": "请先选择一个项目以管理其子智能体", "selectProjectToManageSubagents": "请先选择一个项目以管理其子智能体",
"knowledgeBaseGroup": "知识库",
"moreGroup": "更多",
"knowledgeBases": "知识库管理", "knowledgeBases": "知识库管理",
"voiceSettings": "语音输入配置", "voiceSettings": "语音输入配置",
"embeddingModels": "Embedding 模型" "embeddingModels": "Embedding 模型"