Files
socweb/frontend/components/gemini/GeminiSidebar.tsx
T
gongzhiyongandClaude Sonnet 4.6 04d8fbb740 refactor: 重组项目结构,前端收拢至 frontend/,新增 backend/ 目录
- 前端文件移入 frontend/ 子目录
- 新建 backend/ 目录(待开发)
- 新增 CLAUDE.md、claudehd.md、EXTERNAL_SERVICES.md

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 18:01:04 +08:00

161 lines
5.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { cn } from "@/lib/utils";
import {
PencilLine,
PuzzleIcon,
MoreHorizontal,
Settings,
} from "lucide-react";
interface Conversation {
id: string;
title: string;
}
interface GeminiSidebarProps {
isOpen: boolean;
conversations: Conversation[];
activeConversationId: string | null;
onNewChat: () => void;
onSelectConversation: (id: string) => void;
onOpenExtensions?: () => void;
connectedExtensionsCount?: number;
}
const GemIcon = () => (
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="gemGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#4285f4" />
<stop offset="100%" stopColor="#a855f7" />
</linearGradient>
</defs>
<path
d="M14 2C14 2 16.5 9.5 20 13C23.5 16.5 26 14 26 14C26 14 23.5 16.5 20 20C16.5 23.5 14 26 14 26C14 26 11.5 23.5 8 20C4.5 16.5 2 14 2 14C2 14 4.5 11.5 8 8C11.5 4.5 14 2 14 2Z"
fill="url(#gemGradient)"
/>
</svg>
);
export function GeminiSidebar({
isOpen,
conversations,
activeConversationId,
onNewChat,
onSelectConversation,
onOpenExtensions,
connectedExtensionsCount = 0,
}: GeminiSidebarProps) {
const [hoveredId, setHoveredId] = useState<string | null>(null);
return (
<aside
className={cn(
"fixed left-0 top-0 h-full z-20 flex flex-col transition-all duration-300 ease-in-out",
"bg-[#1e1e1e] border-r border-[#3a3a3a]",
isOpen ? "w-[260px] opacity-100" : "w-0 opacity-0 overflow-hidden pointer-events-none"
)}
aria-label="Sidebar navigation"
>
{/* Logo */}
<div className="flex items-center gap-2.5 px-4 pt-5 pb-4">
<GemIcon />
<span className="text-xl font-semibold bg-gradient-to-r from-[#4285f4] to-[#a855f7] bg-clip-text text-transparent select-none">
运营大脑
</span>
</div>
{/* New Chat Button */}
<div className="px-3 mb-4">
<button
onClick={onNewChat}
className="w-full flex items-center gap-2.5 px-4 py-2.5 rounded-full border border-[#3a3a3a] text-[#e3e3e3] text-sm font-medium hover:bg-[#2a2a2a] transition-colors duration-150 cursor-pointer"
aria-label="新建对话"
>
<PencilLine size={16} className="text-[#9aa0a6]" />
<span>新建对话</span>
</button>
</div>
{/* Recent conversations */}
<div className="flex-1 overflow-y-auto px-2 scrollbar-thin">
<p className="text-xs font-medium text-[#9aa0a6] px-3 mb-2 uppercase tracking-wide">
最近的对话
</p>
<nav aria-label="Recent conversations">
<ul className="space-y-0.5">
{conversations.map((conv) => (
<li key={conv.id}>
<button
onClick={() => onSelectConversation(conv.id)}
onMouseEnter={() => setHoveredId(conv.id)}
onMouseLeave={() => setHoveredId(null)}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm text-left transition-colors duration-150 cursor-pointer group",
activeConversationId === conv.id
? "bg-[#2a2a2a] text-[#e3e3e3]"
: "text-[#c4c7c5] hover:bg-[#2a2a2a] hover:text-[#e3e3e3]"
)}
aria-current={activeConversationId === conv.id ? "page" : undefined}
>
<span className="truncate flex-1">{conv.title}</span>
{hoveredId === conv.id && (
<MoreHorizontal
size={16}
className="flex-shrink-0 ml-1 text-[#9aa0a6] hover:text-[#e3e3e3]"
aria-label="More options"
/>
)}
</button>
</li>
))}
</ul>
</nav>
</div>
{/* Bottom nav */}
<div className="border-t border-[#3a3a3a] pt-2 pb-4 px-2">
<nav aria-label="App navigation">
<ul className="space-y-0.5">
{[
{ icon: PuzzleIcon, label: "扩展程序", onClick: onOpenExtensions },
{ icon: Settings, label: "设置", onClick: undefined },
].map(({ icon: Icon, label, onClick }) => (
<li key={label}>
<button
onClick={onClick}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-[#9aa0a6] hover:bg-[#2a2a2a] hover:text-[#e3e3e3] transition-colors duration-150 cursor-pointer"
>
<Icon size={18} />
<span>{label}</span>
{label === "扩展程序" && connectedExtensionsCount > 0 && (
<span className="ml-auto text-xs bg-[#4285f4] text-white px-1.5 py-0.5 rounded-full">
{connectedExtensionsCount}
</span>
)}
</button>
</li>
))}
</ul>
</nav>
{/* User avatar */}
<div className="flex items-center gap-3 px-3 pt-3 mt-1 border-t border-[#3a3a3a]">
<div
className="w-8 h-8 rounded-full bg-gradient-to-br from-[#4285f4] to-[#a855f7] flex items-center justify-center text-white text-sm font-semibold flex-shrink-0"
aria-label="用户头像"
>
U
</div>
<div className="min-w-0">
<p className="text-sm text-[#e3e3e3] font-medium truncate">用户</p>
<p className="text-xs text-[#9aa0a6] truncate">user@gmail.com</p>
</div>
</div>
</div>
</aside>
);
}