gitdataai/src/components/room/RoomParticipantsPanel.tsx
ZhenYi 00a5369fe1
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
feat(frontend): Discord layout + AI Studio theme + Room Settings
Frontend:
- Add Discord-style 3-column layout (server icons / channel sidebar / chat)
- AI Studio design system: new CSS token palette (--room-* vars)
- Replace all hardcoded Discord colors with CSS variable tokens
- Add RoomSettingsPanel (name, visibility, AI model management)
- Settings + Member list panels mutually exclusive (don't overlap)
- AI models shown at top of member list with green accent
- Fix TS errors: TipTap SuggestionOptions, unused imports, StarterKit options
- Remove MentionInput, MentionPopover, old room components (废弃代码清理)

Backend:
- RoomAiResponse returns model_name from agents.model JOIN
- room_ai_list and room_ai_upsert fetch model name for each config
- AiConfigData ws-protocol interface updated with model_name

Note: RoomSettingsPanel UI still uses shadcn defaults (未完全迁移到AI Studio)
2026-04-18 16:59:36 +08:00

120 lines
3.5 KiB
TypeScript

import { useMemo, memo } from 'react';
import type { RoomMemberResponse } from '@/client';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
import { Loader2, Shield, UserRound } from 'lucide-react';
import type { ReactNode } from 'react';
interface RoomParticipantsPanelProps {
members: RoomMemberResponse[];
membersLoading: boolean;
}
export const RoomParticipantsPanel = memo(function RoomParticipantsPanel({
members,
membersLoading,
}: RoomParticipantsPanelProps) {
// Separate owners/admins from regular members
const admins = useMemo(
() => members.filter((m) => m.role === 'owner' || m.role === 'admin'),
[members],
);
const regular = useMemo(
() => members.filter((m) => m.role === 'member' || m.role === 'guest'),
[members],
);
return (
<aside className="hidden w-[360px] border-l border-border/70 bg-card/50 xl:flex xl:flex-col">
<header className="flex h-12 items-center border-b border-border/70 px-3">
<p className="text-sm font-semibold text-foreground">Members</p>
</header>
{membersLoading ? (
<div className="flex flex-1 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : (
<ScrollArea className="flex-1">
<div className="space-y-5 p-3">
<ParticipantSection
title={`Managers — ${admins.length}`}
icon={<Shield className="h-3.5 w-3.5" />}
>
{admins.map((member) => (
<ParticipantRow
key={member.user}
member={member}
/>
))}
</ParticipantSection>
<ParticipantSection
title={`Members — ${regular.length}`}
icon={<UserRound className="h-3.5 w-3.5" />}
>
{regular.map((member) => (
<ParticipantRow
key={member.user}
member={member}
/>
))}
</ParticipantSection>
</div>
</ScrollArea>
)}
</aside>
);
});
function ParticipantSection({
title,
icon,
children,
}: {
title: string;
icon: ReactNode;
children: ReactNode;
}) {
return (
<section>
<h3 className="mb-2 flex items-center gap-1.5 px-1 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
{icon}
{title}
</h3>
<div className="space-y-1">{children}</div>
</section>
);
}
function ParticipantRow({
member,
}: {
member: RoomMemberResponse;
}) {
const username = member.user_info?.username ?? member.user;
const avatarUrl = member.user_info?.avatar_url;
const displayName = username;
const initial = (displayName?.[0] ?? '?').toUpperCase();
return (
<div
className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors hover:bg-muted/60',
)}
>
<Avatar className="h-7 w-7">
{avatarUrl ? (
<AvatarImage src={avatarUrl} alt={displayName} />
) : null}
<AvatarFallback className="text-xs">{initial}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-foreground">{displayName}</p>
<p className="truncate text-[11px] text-muted-foreground">@{displayName}</p>
</div>
</div>
);
}