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)
120 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|