UI: polish menus
This commit is contained in:
@@ -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" />
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 模型"
|
||||||
|
|||||||
Reference in New Issue
Block a user