Files
openmaic/OpenMAIC/components/scene-renderers/pbl/v2/agent-tabs.tsx
2026-08-16 14:58:47 +08:00

114 lines
4.0 KiB
TypeScript

'use client';
/**
* PBL v2 — Agent tabs.
*
* A container that hosts one or more agent chat threads. The tabs UI is
* only rendered when there is more than one agent — single-agent
* projects (the norm today, with just the Instructor) skip the header
* bar entirely so the workspace doesn't look chatty before it needs to.
*
* The component is intentionally minimal: it just routes to `PBLV2Chat`
* for the active agent. If additional roles are introduced later, each
* tab will mount its own chat component bound to that agent's thread.
*/
import { useMemo, useState } from 'react';
import { GraduationCap, LifeBuoy, SearchCheck, Users } from 'lucide-react';
import type { PBLProjectV2, PBLRoleType } from '@/lib/pbl/v2/types';
import { cn } from '@/lib/utils/cn';
import { PBLV2Chat } from './chat';
import { ScenarioStage } from './scene-stage/scenario-stage';
import type { SubmissionEvaluationStatus } from './submission';
import { useI18n } from '@/lib/hooks/use-i18n';
import type { StreamDisplayState } from './use-instructor-stream';
interface Props {
readonly project: PBLProjectV2;
readonly onProjectChange: (next: PBLProjectV2) => void;
readonly submissionEvaluationStatus?: SubmissionEvaluationStatus | null;
readonly instructorStreaming: boolean;
readonly onInstructorStreamingChange: (active: boolean) => void;
readonly externalStream?: StreamDisplayState | null;
}
export function PBLV2AgentTabs({
project,
onProjectChange,
submissionEvaluationStatus,
instructorStreaming,
onInstructorStreamingChange,
externalStream,
}: Props) {
const { t } = useI18n();
// Stage A only has Instructor; future agents push more entries here.
const agents = useMemo(() => project.roles.filter((r) => r.type !== 'user'), [project.roles]);
const [activeAgentId, setActiveAgentId] = useState<string | undefined>(agents[0]?.id);
const activeAgent = agents.find((a) => a.id === activeAgentId) ?? agents[0];
if (agents.length === 0) {
return (
<div className="flex-1 flex items-center justify-center text-sm text-muted-foreground p-4">
{t('pbl.v2.agentTabs.noAgent')}
</div>
);
}
const showTabsBar = agents.length > 1;
return (
<div className="flex flex-col h-full overflow-hidden">
{showTabsBar && (
<div className="border-b flex items-center gap-1 px-2 py-1.5 bg-muted/30">
{agents.map((agent) => (
<button
key={agent.id}
onClick={() => setActiveAgentId(agent.id)}
className={cn(
'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',
agent.id === activeAgent?.id
? 'bg-card border border-border shadow-sm'
: 'text-muted-foreground hover:bg-card/50',
)}
>
<RoleIcon roleType={agent.type} />
{agent.name}
</button>
))}
</div>
)}
{/* SCENARIO ONLY (increment 5): entrance animation + persistent scene
banner at the top of the chat column. Self-gates to null for
ordinary projects and outside roleplay stages — zero footprint. */}
<ScenarioStage project={project} />
<PBLV2Chat
project={project}
onProjectChange={onProjectChange}
agentName={activeAgent?.name}
submissionEvaluationStatus={submissionEvaluationStatus}
instructorStreaming={instructorStreaming}
onInstructorStreamingChange={onInstructorStreamingChange}
externalStream={externalStream}
/>
</div>
);
}
function RoleIcon({ roleType }: { readonly roleType: PBLRoleType }) {
if (roleType === 'instructor') {
return <GraduationCap className="w-3.5 h-3.5" />;
}
if (roleType === 'evaluator') {
return <SearchCheck className="w-3.5 h-3.5" />;
}
if (roleType === 'mentor') {
return <LifeBuoy className="w-3.5 h-3.5" />;
}
if (roleType === 'collaborator') {
return <Users className="w-3.5 h-3.5" />;
}
return null;
}