feat(ui): update Header and ChannelSidebar components

Refine Header with improved layout, update ChannelSidebar
with channel navigation and ChatPage integration.
This commit is contained in:
ZhenYi 2026-05-14 23:15:40 +08:00
parent c015871024
commit 31e9bb68ac
3 changed files with 69 additions and 49 deletions

View File

@ -10,6 +10,7 @@ import { useProjectInfo } from "@/hooks/useProjectInfo";
import { useConversationQuery } from "@/hooks/useAiChatQuery"; import { useConversationQuery } from "@/hooks/useAiChatQuery";
import { CodePreviewPanel } from "@/components/chat/CodePreviewPanel"; import { CodePreviewPanel } from "@/components/chat/CodePreviewPanel";
import { CodePreviewProvider, type CodePreviewPayload } from "@/components/chat/CodePreviewContext"; import { CodePreviewProvider, type CodePreviewPayload } from "@/components/chat/CodePreviewContext";
import { ProjectJoinBanner, useProjectLayout } from "@/app/project/layout";
interface ChatPageProps { interface ChatPageProps {
scope: "personal" | "project"; scope: "personal" | "project";
@ -27,6 +28,7 @@ export function ChatPage({ scope }: ChatPageProps) {
const [userModel, setSelectedModel] = useState<SelectedModel | null>(null); const [userModel, setSelectedModel] = useState<SelectedModel | null>(null);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true);
const [activeCode, setActiveCode] = useState<CodePreviewPayload | null>(null); const [activeCode, setActiveCode] = useState<CodePreviewPayload | null>(null);
const { isProjectPreview } = useProjectLayout();
const { data: conversation } = useConversationQuery(selectedConversationId || ""); const { data: conversation } = useConversationQuery(selectedConversationId || "");
@ -131,24 +133,36 @@ export function ChatPage({ scope }: ChatPageProps) {
{selectedConversationId ? ( {selectedConversationId ? (
<> <>
<ChatMessageList conversationId={selectedConversationId} setIsStreaming={setIsStreaming} /> <ChatMessageList conversationId={selectedConversationId} setIsStreaming={setIsStreaming} />
<ChatMessageInput {scope === "project" && isProjectPreview ? (
conversationId={selectedConversationId} <div className="shrink-0 px-4 pb-4">
isStreaming={isStreaming} <div className="mx-auto max-w-3xl">
setIsStreaming={setIsStreaming} <ProjectJoinBanner compact message="Join this project to start project chat." />
onSelectConversation={handleSelectConversation} </div>
/> </div>
) : (
<ChatMessageInput
conversationId={selectedConversationId}
isStreaming={isStreaming}
setIsStreaming={setIsStreaming}
onSelectConversation={handleSelectConversation}
/>
)}
</> </>
) : ( ) : (
<div className="flex-1 flex flex-col items-center justify-center px-4 gap-4"> <div className="flex-1 flex flex-col items-center justify-center px-4 gap-4">
<div className="w-full max-w-3xl"> <div className="w-full max-w-3xl">
<ChatMessageList conversationId={null} setIsStreaming={setIsStreaming} /> <ChatMessageList conversationId={null} setIsStreaming={setIsStreaming} />
<div className="mt-4"> <div className="mt-4">
<ChatMessageInput {scope === "project" && isProjectPreview ? (
conversationId={null} <ProjectJoinBanner compact message="Join this project to start project chat." />
isStreaming={isStreaming} ) : (
setIsStreaming={setIsStreaming} <ChatMessageInput
onSelectConversation={handleSelectConversation} conversationId={null}
/> isStreaming={isStreaming}
setIsStreaming={setIsStreaming}
onSelectConversation={handleSelectConversation}
/>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -41,8 +41,9 @@ interface ChannelSidebarProps {
export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: ChannelSidebarProps) { export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: ChannelSidebarProps) {
const location = useLocation(); const location = useLocation();
const {projectName} = useParams<{ projectName: string }>(); const {projectName} = useParams<{ projectName: string }>();
const {data: roomsData, isLoading} = useRoomsQuery(projectName);
const {data: projectInfo} = useProjectInfo(projectName); const {data: projectInfo} = useProjectInfo(projectName);
const isProjectMember = !!projectInfo?.role;
const {data: roomsData, isLoading} = useRoomsQuery(isProjectMember ? projectName : undefined);
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false); const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
const rooms = useMemo(() => roomsData?.rooms ?? [], [roomsData?.rooms]); const rooms = useMemo(() => roomsData?.rooms ?? [], [roomsData?.rooms]);
@ -103,14 +104,16 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
> >
<Search className="w-[14px] h-[14px]"/> <Search className="w-[14px] h-[14px]"/>
</button> </button>
<button {isProjectMember && (
onClick={() => setIsCreateMenuOpen(true)} <button
className={CHANNEL_SIDEBAR.iconButton} onClick={() => setIsCreateMenuOpen(true)}
style={{color: "var(--text-secondary)"}} className={CHANNEL_SIDEBAR.iconButton}
title="Create new..." style={{color: "var(--text-secondary)"}}
> title="Create new..."
<Plus className="w-[14px] h-[14px]"/> >
</button> <Plus className="w-[14px] h-[14px]"/>
</button>
)}
{onCollapse && ( {onCollapse && (
<button <button
onClick={onCollapse} onClick={onCollapse}
@ -178,7 +181,7 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
</Link> </Link>
)} )}
{isLoading ? ( {!isProjectMember ? null : isLoading ? (
<div className="px-4 py-2 text-[var(--text-muted)]">Loading channels...</div> <div className="px-4 py-2 text-[var(--text-muted)]">Loading channels...</div>
) : ( ) : (
<> <>

View File

@ -39,6 +39,7 @@ const ME_NAV_SIBLINGS: BreadcrumbSibling[] = [
{ label: "Stars", path: "/me/stars" }, { label: "Stars", path: "/me/stars" },
{ label: "Following", path: "/me/following" }, { label: "Following", path: "/me/following" },
{ label: "Followers", path: "/me/followers" }, { label: "Followers", path: "/me/followers" },
{ label: "Invitations", path: "/me/invitations" },
]; ];
function getProjectNavSiblings(projectName: string): BreadcrumbSibling[] { function getProjectNavSiblings(projectName: string): BreadcrumbSibling[] {
@ -167,7 +168,7 @@ const TOOLBAR_ICONS = [
export const Header = memo(function Header() { export const Header = memo(function Header() {
const location = useLocation(); const location = useLocation();
const { segments, projects } = useBreadcrumbs(); const { segments, projects } = useBreadcrumbs();
const { showMembers, setShowMembers } = useProjectLayout(); const { isProjectMember, showMembers, setShowMembers } = useProjectLayout();
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const roomContext = useOptionalRoom(); const roomContext = useOptionalRoom();
@ -254,7 +255,7 @@ export const Header = memo(function Header() {
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{location.pathname.startsWith("/me") ? null : ( {location.pathname.startsWith("/me") ? null : (
<> <>
{roomContext?.currentRoom && location.pathname.includes("/channel/") && ( {isProjectMember && roomContext?.currentRoom && location.pathname.includes("/channel/") && (
<button <button
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors hover:bg-hover-bg" className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors hover:bg-hover-bg"
@ -264,32 +265,34 @@ export const Header = memo(function Header() {
<Settings className="w-[18px] h-[18px]" /> <Settings className="w-[18px] h-[18px]" />
</button> </button>
)} )}
<button {isProjectMember && (
onClick={() => setShowMembers(!showMembers)} <button
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors" onClick={() => setShowMembers(!showMembers)}
style={ className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
showMembers style={
? { showMembers
color: "var(--text-primary)", ? {
backgroundColor: "var(--hover-bg-strong)", color: "var(--text-primary)",
} backgroundColor: "var(--hover-bg-strong)",
: { color: "var(--text-secondary)" } }
} : { color: "var(--text-secondary)" }
> }
<svg
className="w-[18px] h-[18px]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
> >
<path <svg
strokeLinecap="round" className="w-[18px] h-[18px]"
strokeLinejoin="round" fill="none"
strokeWidth={1.5} stroke="currentColor"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" viewBox="0 0 24 24"
/> >
</svg> <path
</button> strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</button>
)}
{TOOLBAR_ICONS.map((icon, i) => ( {TOOLBAR_ICONS.map((icon, i) => (
<button <button