Commit 906063ee authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat(team): add AI workforce team management hub, gradient circle avatars, and...

feat(team): add AI workforce team management hub, gradient circle avatars, and collaboration flow DAG
parent 458d41da
Pipeline #3907 failed with stage
......@@ -19,12 +19,23 @@ export interface ChannelItem {
export interface AgentMember {
id: string;
name: string;
shortName: string;
initial: string;
role: "Founder" | "PM" | "Architect" | "Coder" | "QA";
avatar: string;
title: string;
department: string;
avatarGradient: string;
avatarColor: string;
model: string;
skills: string[];
status: "online" | "thinking" | "busy";
activeTask?: string;
stats?: {
tasksCompleted: number;
testsPassed: string;
avgSpeed: string;
approvalRate: string;
};
}
export interface ModelItem {
......@@ -123,53 +134,108 @@ export const MOCK_CHANNELS: Record<string, ChannelItem[]> = {
export const MOCK_AGENTS: AgentMember[] = [
{
id: "agent-founder",
name: "Thomas Shelby (Founder)",
name: "Thomas Shelby",
shortName: "Thomas",
initial: "T",
role: "Founder",
avatar: "👑",
title: "CEO & Founder",
department: "Executive HQ",
avatarGradient: "from-amber-500 to-orange-600",
avatarColor: "#F59E0B",
model: "claude-3-7-sonnet",
skills: ["/goal", "/export-doc", "/teamwork-preview", "/approve"],
status: "online",
activeTask: "Reviewing company-wide pipeline gates",
stats: {
tasksCompleted: 48,
testsPassed: "100%",
avgSpeed: "1.1s",
approvalRate: "96%",
},
},
{
id: "agent-pm",
name: "PM Strategist Bot",
name: "Minh Quang",
shortName: "Minh",
initial: "M",
role: "PM",
avatar: "📊",
title: "Lead Product Manager",
department: "Product & Spec",
avatarGradient: "from-indigo-500 to-purple-600",
avatarColor: "#6366F1",
model: "claude-3-7-sonnet",
skills: ["/breakdown", "/spec", "/prd-generator"],
status: "online",
activeTask: "Synthesizing User Stories from Founder vision",
stats: {
tasksCompleted: 64,
testsPassed: "100%",
avgSpeed: "1.4s",
approvalRate: "94%",
},
},
{
id: "agent-architect",
name: "System Architect Bot",
name: "An Nguyen",
shortName: "An",
initial: "A",
role: "Architect",
avatar: "🏛️",
title: "Principal System Architect",
department: "System Architecture",
avatarGradient: "from-cyan-500 to-teal-600",
avatarColor: "#06B6D4",
model: "o1",
skills: ["/schema-design", "/api-contract", "/dag-planner"],
status: "online",
activeTask: "Designing idempotent webhook pipeline & schemas",
stats: {
tasksCompleted: 52,
testsPassed: "100%",
avgSpeed: "2.1s",
approvalRate: "98%",
},
},
{
id: "agent-coder",
name: "Lead Coder Bot",
name: "Đức Anh",
shortName: "Đức",
initial: "Đ",
role: "Coder",
avatar: "💻",
title: "Lead Software Engineer",
department: "Engineering",
avatarGradient: "from-emerald-500 to-green-600",
avatarColor: "#10B981",
model: "deepseek-reasoner",
skills: ["/code-gen", "/refactor", "/unit-test-builder"],
status: "online",
activeTask: "Generating test-driven implementation and pull request diffs",
stats: {
tasksCompleted: 112,
testsPassed: "100%",
avgSpeed: "1.8s",
approvalRate: "95%",
},
},
{
id: "agent-qa",
name: "QA & Security Auditor",
name: "Thu Hà",
shortName: "Hà",
initial: "H",
role: "QA",
avatar: "🛡️",
title: "Lead Security Auditor",
department: "Security & Compliance",
avatarGradient: "from-rose-500 to-pink-600",
avatarColor: "#F43F5E",
model: "claude-3-7-sonnet",
skills: ["/security-audit", "/stress-test", "/adversarial-suite"],
status: "online",
activeTask: "Conducting zero-trust gate verification",
stats: {
tasksCompleted: 86,
testsPassed: "100%",
avgSpeed: "1.3s",
approvalRate: "99%",
},
},
];
......
import { TeamManagementPage } from "../../team/team-management-page";
/** @jsxImportSource react */
import type { CSSProperties } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
......@@ -339,6 +340,17 @@ export function SessionPage(props: SessionPageProps) {
));
const sessionPanelState = useSessionPanelState(sidePanelSessionKey);
const activePanelTab = useActivePanelTab(sidePanelSessionKey);
const [teamViewOpen, setTeamViewOpen] = useState(false);
useEffect(() => {
const handleToggle = () => setTeamViewOpen((prev) => !prev);
const handleClose = () => setTeamViewOpen(false);
window.addEventListener("openwork-open-team", handleToggle);
window.addEventListener("openwork-close-team", handleClose);
return () => {
window.removeEventListener("openwork-open-team", handleToggle);
window.removeEventListener("openwork-close-team", handleClose);
};
}, []);
const [hiddenTargetRevision, setHiddenTargetRevision] = useState(0);
const [, setExtensionStateVersion] = useState(0);
const hiddenAccessibleTargetIds = useMemo(
......@@ -1287,6 +1299,14 @@ export function SessionPage(props: SessionPageProps) {
<ResizablePanelGroup orientation="vertical" className="min-h-0 flex-1 overflow-hidden">
<ResizablePanel minSize="180px" className="min-h-0">
<div className="relative h-full min-w-0 overflow-hidden bg-dls-surface mac:bg-dls-surface/85 mac:backdrop-blur-2xl mac:backdrop-saturate-150">
{teamViewOpen ? (
<div className="absolute inset-0 z-30 overflow-y-auto bg-background animate-in fade-in duration-200">
<TeamManagementPage
workspaceName={props.selectedWorkspaceName || "AI Software Studio"}
onOpenChat={() => setTeamViewOpen(false)}
/>
</div>
) : null}
{props.primarySlot ? (
<div className="h-full overflow-y-auto" data-workspace-primary-slot>
{props.primarySlot}
......
......@@ -12,6 +12,7 @@ import {
Code2,
Layers,
ShieldCheck,
Settings,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { AgentMember } from "@/app/lib/mock-data";
......@@ -156,6 +157,17 @@ export function WorkforceList({
</button>
}
/>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
window.dispatchEvent(new CustomEvent("openwork-open-team"));
}}
title="Quản lý tổ chức AI (Team Hub)"
className="ml-1 flex size-5 cursor-pointer items-center justify-center rounded text-muted-foreground/60 transition hover:bg-sidebar-accent hover:text-foreground"
>
<Settings className="size-3" />
</button>
</div>
<CollapsibleContent className="mt-1">
......@@ -163,8 +175,9 @@ export function WorkforceList({
{agents.map((agent) => {
const statusMeta = getStatusBadge(agent.status);
const isSelected = activeAgentId === agent.id;
const iconMeta = getAgentIcon(agent);
const IconComponent = iconMeta.Icon;
const initial = agent.initial || agent.name.charAt(0);
const shortName = agent.shortName || agent.name.split(" ")[0];
const gradientClass = agent.avatarGradient || "from-blue-500 to-indigo-600";
return (
<SidebarMenuItem key={agent.id}>
......@@ -177,14 +190,19 @@ export function WorkforceList({
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground shadow-xs"
: "text-sidebar-foreground/80 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
)}
title={`${agent.name} (${agent.role}) - ${agent.department}\nModel: ${agent.model}\nStatus: ${statusMeta.label}`}
title={`${agent.name} (${agent.title || agent.role}) - ${agent.department}\nModel: ${agent.model}\nStatus: ${statusMeta.label}`}
>
<SidebarGlyphSlot>
<span className={cn("relative flex size-4 items-center justify-center rounded", iconMeta.bg)}>
<IconComponent className={cn("size-3.5", iconMeta.color)} />
<span
className={cn(
"relative flex size-5 shrink-0 items-center justify-center rounded-full font-semibold text-[10px] text-white shadow-xs bg-gradient-to-tr",
gradientClass,
)}
>
{initial}
<span
className={cn(
"absolute -bottom-0.5 -right-0.5 size-1.5 rounded-full border border-sidebar-background",
"absolute -bottom-0.5 -right-0.5 size-2 rounded-full border-2 border-sidebar-background ring-1 ring-background/50",
statusMeta.className,
)}
title={statusMeta.label}
......@@ -192,13 +210,15 @@ export function WorkforceList({
</span>
</SidebarGlyphSlot>
<div className="flex min-w-0 flex-1 items-center justify-between gap-1">
<span className="truncate text-foreground/90 font-medium">
{agent.name.split(" ")[0]}
<span className="ml-1 text-[11px] font-normal text-muted-foreground/70">
{agent.role}
<div className="flex min-w-0 flex-1 items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-1 truncate">
<span className="font-medium text-foreground">
{shortName}
</span>
</span>
<span className="text-[11px] text-muted-foreground/70 truncate">
· {agent.role}
</span>
</div>
<span
className="shrink-0 rounded border border-border/40 bg-muted/40 px-1 py-0.2 font-mono text-[9px] text-muted-foreground/80"
......
import { TeamManagementPage } from "../domains/team/team-management-page";
/** @jsxImportSource react */
import {
useCallback,
......@@ -1258,6 +1259,18 @@ export function SessionRoute() {
}, [navigate, selectedSessionId, sidebarActiveWorkspaceId]);
const extensionsMainOpen = /^\/(?:workspace\/[^/]+\/)?extensions(?:\/|$)/.test(location.pathname);
const teamMainOpen = /^\/(?:workspace\/[^/]+\/)?team(?:\/|$)/.test(location.pathname);
const [showTeamView, setShowTeamView] = useState(false);
useEffect(() => {
const handleOpenTeam = () => setShowTeamView(true);
const handleCloseTeam = () => setShowTeamView(false);
window.addEventListener("openwork-open-team", handleOpenTeam);
window.addEventListener("openwork-close-team", handleCloseTeam);
return () => {
window.removeEventListener("openwork-open-team", handleOpenTeam);
window.removeEventListener("openwork-close-team", handleCloseTeam);
};
}, []);
const surfaceProps = useMemo(() => {
if (!selectedWorkspaceId || !selectedSessionId) {
......@@ -2905,7 +2918,16 @@ export function SessionRoute() {
}}
notFoundMessage={gatedRouteNotFoundMessage}
mainContentTakeover={
extensionsMainOpen ? (
(showTeamView || teamMainOpen) ? (
<TeamManagementPage
workspaceName={selectedWorkspace?.displayName || selectedWorkspace?.name || "AI Software Studio"}
onOpenChat={() => {
if (selectedWorkspaceId && selectedSessionId) {
navigateToWorkspaceSession(selectedWorkspaceId, selectedSessionId);
}
}}
/>
) : extensionsMainOpen ? (
<SettingsSurface
standaloneExtensions
workspaceId={selectedWorkspaceId || undefined}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment