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

View File

@ -41,8 +41,9 @@ interface ChannelSidebarProps {
export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: ChannelSidebarProps) {
const location = useLocation();
const {projectName} = useParams<{ projectName: string }>();
const {data: roomsData, isLoading} = useRoomsQuery(projectName);
const {data: projectInfo} = useProjectInfo(projectName);
const isProjectMember = !!projectInfo?.role;
const {data: roomsData, isLoading} = useRoomsQuery(isProjectMember ? projectName : undefined);
const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
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]"/>
</button>
<button
onClick={() => setIsCreateMenuOpen(true)}
className={CHANNEL_SIDEBAR.iconButton}
style={{color: "var(--text-secondary)"}}
title="Create new..."
>
<Plus className="w-[14px] h-[14px]"/>
</button>
{isProjectMember && (
<button
onClick={() => setIsCreateMenuOpen(true)}
className={CHANNEL_SIDEBAR.iconButton}
style={{color: "var(--text-secondary)"}}
title="Create new..."
>
<Plus className="w-[14px] h-[14px]"/>
</button>
)}
{onCollapse && (
<button
onClick={onCollapse}
@ -178,7 +181,7 @@ export const ChannelSidebar = memo(function ChannelSidebar({onCollapse}: Channel
</Link>
)}
{isLoading ? (
{!isProjectMember ? null : isLoading ? (
<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: "Following", path: "/me/following" },
{ label: "Followers", path: "/me/followers" },
{ label: "Invitations", path: "/me/invitations" },
];
function getProjectNavSiblings(projectName: string): BreadcrumbSibling[] {
@ -167,7 +168,7 @@ const TOOLBAR_ICONS = [
export const Header = memo(function Header() {
const location = useLocation();
const { segments, projects } = useBreadcrumbs();
const { showMembers, setShowMembers } = useProjectLayout();
const { isProjectMember, showMembers, setShowMembers } = useProjectLayout();
const [showSettings, setShowSettings] = useState(false);
const roomContext = useOptionalRoom();
@ -254,7 +255,7 @@ export const Header = memo(function Header() {
<div className="flex items-center gap-1 shrink-0">
{location.pathname.startsWith("/me") ? null : (
<>
{roomContext?.currentRoom && location.pathname.includes("/channel/") && (
{isProjectMember && roomContext?.currentRoom && location.pathname.includes("/channel/") && (
<button
onClick={() => setShowSettings(true)}
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]" />
</button>
)}
<button
onClick={() => setShowMembers(!showMembers)}
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
style={
showMembers
? {
color: "var(--text-primary)",
backgroundColor: "var(--hover-bg-strong)",
}
: { color: "var(--text-secondary)" }
}
>
<svg
className="w-[18px] h-[18px]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
{isProjectMember && (
<button
onClick={() => setShowMembers(!showMembers)}
className="w-8 h-8 flex items-center justify-center rounded-[4px] transition-colors"
style={
showMembers
? {
color: "var(--text-primary)",
backgroundColor: "var(--hover-bg-strong)",
}
: { color: "var(--text-secondary)" }
}
>
<path
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>
<svg
className="w-[18px] h-[18px]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
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) => (
<button