423 lines
10 KiB
TypeScript
423 lines
10 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import styled from 'styled-components';
|
|
import Link from 'next/link';
|
|
import SisterAvatar from '@/components/common/SisterAvatar';
|
|
import type { SisterNode, SubAgent, SisterName, AgentState, SelectedAgent } from './OfficeScene';
|
|
|
|
interface ContextPanelProps {
|
|
selected: SelectedAgent | null;
|
|
sisters: SisterNode[];
|
|
onOpenChat: (sisterName: SisterName) => void;
|
|
onClear: () => void;
|
|
}
|
|
|
|
const Panel = styled.aside`
|
|
width: 300px;
|
|
flex-shrink: 0;
|
|
border-left: 1px solid var(--border-color);
|
|
background: var(--bg-surface);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
|
|
@media (max-width: 1199px) {
|
|
width: 260px;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
width: 100%;
|
|
border-left: none;
|
|
border-top: 1px solid var(--border-color);
|
|
max-height: 260px;
|
|
}
|
|
`;
|
|
|
|
const PanelHeader = styled.div`
|
|
padding: var(--space-md) var(--space-lg);
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-shrink: 0;
|
|
`;
|
|
|
|
const PanelTitle = styled.div`
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
`;
|
|
|
|
const ClearBtn = styled.button`
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-secondary);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
line-height: 1;
|
|
|
|
&:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
`;
|
|
|
|
const PanelBody = styled.div`
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--space-lg);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-lg);
|
|
`;
|
|
|
|
const EmptyState = styled.div`
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
text-align: center;
|
|
padding: var(--space-xl) 0;
|
|
`;
|
|
|
|
const AgentHeader = styled.div`
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-md);
|
|
`;
|
|
|
|
const AgentAvatarWrap = styled.div`
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
`;
|
|
|
|
const AgentMeta = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
`;
|
|
|
|
const AgentName = styled.div`
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
`;
|
|
|
|
const AgentRole = styled.div`
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
`;
|
|
|
|
const AgentType = styled.div`
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
`;
|
|
|
|
const StateRow = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
`;
|
|
|
|
const StateDot = styled.span<{ $state: AgentState }>`
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
background: ${({ $state }) => {
|
|
const colors: Record<AgentState, string> = {
|
|
idle: '#444',
|
|
thinking: '#2979FF',
|
|
tool_calling: '#FF9800',
|
|
speaking: '#00BFA5',
|
|
error: '#FF1744',
|
|
};
|
|
return colors[$state];
|
|
}};
|
|
`;
|
|
|
|
const StateLabel = styled.span<{ $state: AgentState }>`
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: ${({ $state }) => {
|
|
const colors: Record<AgentState, string> = {
|
|
idle: 'var(--text-secondary)',
|
|
thinking: '#2979FF',
|
|
tool_calling: '#FF9800',
|
|
speaking: '#00BFA5',
|
|
error: '#FF1744',
|
|
};
|
|
return colors[$state];
|
|
}};
|
|
`;
|
|
|
|
const Section = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm);
|
|
`;
|
|
|
|
const SectionLabel = styled.div`
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding-bottom: 4px;
|
|
`;
|
|
|
|
const SectionValue = styled.div`
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
`;
|
|
|
|
const CurrentTaskBox = styled.div`
|
|
font-size: 12px;
|
|
color: #58A6FF;
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: rgba(88, 166, 255, 0.06);
|
|
border-left: 2px solid #58A6FF;
|
|
line-height: 1.5;
|
|
`;
|
|
|
|
const SubagentGrid = styled.div`
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
`;
|
|
|
|
const SubagentRow = styled.div`
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
`;
|
|
|
|
const SubDot = styled.span<{ $state: AgentState }>`
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
background: ${({ $state }) => {
|
|
const colors: Record<AgentState, string> = {
|
|
idle: '#444',
|
|
thinking: '#2979FF',
|
|
tool_calling: '#FF9800',
|
|
speaking: '#00BFA5',
|
|
error: '#FF1744',
|
|
};
|
|
return colors[$state];
|
|
}};
|
|
`;
|
|
|
|
const SubName = styled.span`
|
|
color: var(--text-secondary);
|
|
font-size: 11px;
|
|
font-family: var(--font-mono);
|
|
`;
|
|
|
|
const ActionRow = styled.div`
|
|
display: flex;
|
|
gap: var(--space-sm);
|
|
flex-wrap: wrap;
|
|
`;
|
|
|
|
const ActionBtn = styled.button`
|
|
border: 1px solid var(--border-color);
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: var(--space-xs) var(--space-sm);
|
|
cursor: pointer;
|
|
font-family: var(--font-mono);
|
|
transition: all 0.15s;
|
|
|
|
&:hover {
|
|
border-color: var(--border-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
`;
|
|
|
|
const ActionLink = styled(Link)`
|
|
border: 1px solid var(--border-color);
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
padding: var(--space-xs) var(--space-sm);
|
|
text-decoration: none;
|
|
font-family: var(--font-mono);
|
|
transition: all 0.15s;
|
|
display: inline-block;
|
|
|
|
&:hover {
|
|
border-color: var(--border-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
`;
|
|
|
|
const FallbackNote = styled.div`
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
opacity: 0.7;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
`;
|
|
|
|
const SISTER_DISPLAY: Record<SisterName, string> = {
|
|
harang: '하랑이',
|
|
narang: '나랑이',
|
|
darang: '다랑이',
|
|
erang: '이랑이',
|
|
};
|
|
|
|
function SisterDetail({ sister, onOpenChat }: { sister: SisterNode; onOpenChat: (name: SisterName) => void }) {
|
|
const hasLiveSubagentState = sister.subagents.some((item) => item.sessionLabel || item.currentTask || item.updatedAt);
|
|
|
|
return (
|
|
<>
|
|
<AgentHeader>
|
|
<AgentAvatarWrap>
|
|
<SisterAvatar name={sister.name} size={40} />
|
|
</AgentAvatarWrap>
|
|
<AgentMeta>
|
|
<AgentName>{SISTER_DISPLAY[sister.name]}</AgentName>
|
|
<AgentRole>{sister.role}</AgentRole>
|
|
<AgentType>main agent</AgentType>
|
|
</AgentMeta>
|
|
</AgentHeader>
|
|
|
|
<StateRow>
|
|
<StateDot $state={sister.state} />
|
|
<StateLabel $state={sister.state}>{sister.state}</StateLabel>
|
|
</StateRow>
|
|
|
|
{(sister.currentTask || sister.activeSessionLabel) && (
|
|
<Section>
|
|
<SectionLabel>current task</SectionLabel>
|
|
<CurrentTaskBox>{sister.currentTask ?? sister.activeSessionLabel ?? '작업 정보 없음'}</CurrentTaskBox>
|
|
</Section>
|
|
)}
|
|
|
|
<Section>
|
|
<SectionLabel>subagents ({sister.subagents.length})</SectionLabel>
|
|
<SubagentGrid>
|
|
{sister.subagents.map((sub) => (
|
|
<SubagentRow key={sub.id}>
|
|
<SubDot $state={sub.state} />
|
|
<span>{sub.label}</span>
|
|
<SubName>· {sub.state}</SubName>
|
|
</SubagentRow>
|
|
))}
|
|
</SubagentGrid>
|
|
<FallbackNote>{hasLiveSubagentState ? 'subagent state: live runtime' : 'subagent state: fallback'}</FallbackNote>
|
|
</Section>
|
|
|
|
<ActionRow>
|
|
<ActionBtn onClick={() => onOpenChat(sister.name)}>채팅</ActionBtn>
|
|
<ActionLink href={`/sisters/${sister.name}`}>상세</ActionLink>
|
|
</ActionRow>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function SubagentDetail({
|
|
sub,
|
|
sister,
|
|
onOpenChat,
|
|
}: {
|
|
sub: SubAgent;
|
|
sister: SisterNode;
|
|
onOpenChat: (name: SisterName) => void;
|
|
}) {
|
|
const liveNote = sub.sessionLabel || sub.currentTask || sub.updatedAt;
|
|
|
|
return (
|
|
<>
|
|
<AgentHeader>
|
|
<AgentAvatarWrap>
|
|
<SisterAvatar name={sister.name} size={40} />
|
|
</AgentAvatarWrap>
|
|
<AgentMeta>
|
|
<AgentName>{sub.label}</AgentName>
|
|
<AgentRole>{sub.name}</AgentRole>
|
|
<AgentType>subagent · {SISTER_DISPLAY[sister.name]} 소속</AgentType>
|
|
</AgentMeta>
|
|
</AgentHeader>
|
|
|
|
<StateRow>
|
|
<StateDot $state={sub.state} />
|
|
<StateLabel $state={sub.state}>{sub.state}</StateLabel>
|
|
</StateRow>
|
|
|
|
<Section>
|
|
<SectionLabel>parent</SectionLabel>
|
|
<SectionValue>{SISTER_DISPLAY[sister.name]} · {sister.role}</SectionValue>
|
|
</Section>
|
|
|
|
{(sub.currentTask || sub.sessionLabel) && (
|
|
<Section>
|
|
<SectionLabel>current task</SectionLabel>
|
|
<CurrentTaskBox>{sub.currentTask ?? sub.sessionLabel ?? '작업 정보 없음'}</CurrentTaskBox>
|
|
</Section>
|
|
)}
|
|
|
|
<FallbackNote>{liveNote ? 'subagent state: live runtime' : 'subagent state: fallback · no direct api'}</FallbackNote>
|
|
|
|
<ActionRow>
|
|
<ActionBtn onClick={() => onOpenChat(sister.name)}>{SISTER_DISPLAY[sister.name]} 채팅</ActionBtn>
|
|
<ActionLink href={`/sisters/${sister.name}`}>자매 상세</ActionLink>
|
|
</ActionRow>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ContextPanel({ selected, sisters, onOpenChat, onClear }: ContextPanelProps) {
|
|
const getSister = (name: SisterName) => sisters.find((s) => s.name === name);
|
|
|
|
let content: React.ReactNode = (
|
|
<EmptyState>
|
|
에이전트를 선택하면
|
|
<br />
|
|
상세 정보가 표시됩니다.
|
|
</EmptyState>
|
|
);
|
|
|
|
if (selected?.type === 'sister') {
|
|
const sister = getSister(selected.name);
|
|
if (sister) content = <SisterDetail sister={sister} onOpenChat={onOpenChat} />;
|
|
} else if (selected?.type === 'subagent') {
|
|
const sister = getSister(selected.sister);
|
|
const sub = sister?.subagents.find((item) => item.id === selected.id);
|
|
if (sister && sub) content = <SubagentDetail sub={sub} sister={sister} onOpenChat={onOpenChat} />;
|
|
}
|
|
|
|
return (
|
|
<Panel>
|
|
<PanelHeader>
|
|
<PanelTitle>context panel</PanelTitle>
|
|
{selected && <ClearBtn onClick={onClear}>✕</ClearBtn>}
|
|
</PanelHeader>
|
|
<PanelBody>{content}</PanelBody>
|
|
</Panel>
|
|
);
|
|
}
|