103 lines
7.9 KiB
TypeScript
103 lines
7.9 KiB
TypeScript
import { ArrowUpRight, Loader2, RefreshCw, X } from 'lucide-react';
|
||
import { useState, type CSSProperties, type Ref } from 'react';
|
||
import { Provider as TooltipProvider } from '@radix-ui/react-tooltip';
|
||
import { TeacherAvatar } from './TeacherAvatar';
|
||
import { TeacherIntroduction } from './TeacherIntroduction';
|
||
import type { useTeacherCompanion } from './use-teacher-companion';
|
||
|
||
type Companion = ReturnType<typeof useTeacherCompanion>;
|
||
export function TeacherCompanion({ companion, open, onOpen, onToggle, bubbleRef, introductionRef }: {
|
||
companion: Companion;
|
||
open: boolean;
|
||
onOpen(agentId?: string): void;
|
||
onToggle?(agentId?: string): void;
|
||
bubbleRef?: Ref<HTMLDivElement>;
|
||
introductionRef?: Ref<HTMLDivElement>;
|
||
}) {
|
||
const { definition, invitation, welcome, welcomes, loading, enabled, checking, error } = companion;
|
||
const [introductionId, setIntroductionId] = useState<string | null>(null);
|
||
const introductionProps = (id: string) => ({
|
||
open: introductionId === id,
|
||
onOpenChange: (next: boolean) => setIntroductionId((current) => next ? id : current === id ? null : current),
|
||
});
|
||
const name = definition?.name || '智能体';
|
||
const activeId = companion.selectedAgent?.teacher_id;
|
||
const agents = companion.selectedAgent && !companion.agents.some((item) => item.teacher_id === activeId)
|
||
? [...companion.agents, companion.selectedAgent] : companion.agents;
|
||
const viewConversation = () => { companion.dismiss(); onOpen(); };
|
||
const meetTeacher = (agentId: string) => { setIntroductionId(null); companion.dismissWelcomes(); onOpen(agentId); };
|
||
const status = loading ? '正在连接智能体…'
|
||
: checking ? '智能体在看看你最近的进展…'
|
||
: error || (!enabled ? '智能体暂未开放,仍可查看以往讨论' : open ? '我们慢慢聊,想到什么都可以说' : '点我聊聊,想到什么都可以说');
|
||
const refreshMessage = companion.refreshing ? '正在刷新…'
|
||
: companion.refreshStatus === 'error' ? '刷新失败,请重试'
|
||
: companion.refreshStatus === 'success' ? '已更新' : '';
|
||
return (
|
||
<TooltipProvider delayDuration={180} skipDelayDuration={100}>
|
||
<section className="teacher-companion no-drag" aria-label="智能体陪伴" data-testid="teacher-companion">
|
||
<nav className="consultation-agent-tabs" aria-label="已下发智能体">
|
||
{agents.map((agent) => <TeacherIntroduction key={agent.teacher_id} definition={agent.definition} contentRef={introductionRef} {...introductionProps(`agent:${agent.teacher_id}`)}>
|
||
<button type="button" className="teacher-companion-person"
|
||
aria-label={agents.length === 1 ? '智能体' : `与${agent.definition.name}聊天`}
|
||
aria-expanded={open && activeId === agent.teacher_id} aria-pressed={open && activeId === agent.teacher_id}
|
||
aria-controls="coding-consultation-dock" data-agent-id={agent.teacher_id}
|
||
onMouseDown={(event) => event.preventDefault()} onClick={() => (onToggle ?? onOpen)(agent.teacher_id)}>
|
||
<span className="teacher-companion-avatar"><TeacherAvatar definition={agent.definition} />
|
||
{(companion.unreadAgentIds.includes(agent.teacher_id) || (activeId === agent.teacher_id && invitation)) && <span className="teacher-companion-unread" aria-hidden="true" />}</span>
|
||
<span className="teacher-companion-name">{agent.definition.name}</span>
|
||
{activeId === agent.teacher_id && checking && <Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none" aria-label="智能体正在思考" />}
|
||
</button></TeacherIntroduction>)}
|
||
{(!agents.length || (!activeId && companion.topic)) && <TeacherIntroduction definition={definition} contentRef={introductionRef} {...introductionProps('history')}>
|
||
<button type="button" className="teacher-companion-person" aria-description={status} aria-label="智能体" aria-expanded={open} aria-controls="coding-consultation-dock" onMouseDown={(event) => event.preventDefault()} onClick={() => (onToggle ?? onOpen)()}>
|
||
<span className="teacher-companion-avatar"><TeacherAvatar definition={definition} />{invitation && <span className="teacher-companion-unread" aria-hidden="true" />}</span>
|
||
<span className="teacher-companion-name">{name}</span>
|
||
{checking && <Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground motion-reduce:animate-none" aria-label="智能体正在思考" />}
|
||
</button></TeacherIntroduction>}
|
||
</nav>
|
||
<button type="button" className="consultation-agents-refresh" aria-label="刷新智能体"
|
||
title={refreshMessage || '刷新智能体'} disabled={loading || companion.refreshing} aria-busy={companion.refreshing}
|
||
onMouseDown={(event) => event.preventDefault()} onClick={() => { setIntroductionId(null); void companion.refresh(); }}>
|
||
<RefreshCw className={`h-3.5 w-3.5 shrink-0 ${companion.refreshing ? 'animate-spin motion-reduce:animate-none' : ''}`} aria-hidden="true" />
|
||
</button>
|
||
<span className="sr-only" role="status" aria-live="polite">{refreshMessage}</span>
|
||
{!invitation && welcomes.length > 0 && <div ref={bubbleRef} className="teacher-welcome-presence" data-testid="teacher-welcomes" aria-label="认识这里的老师">
|
||
<div className="teacher-welcome-list">
|
||
{welcomes.map((item, index) => <article key={item.agentId} className="teacher-welcome-card" data-agent-id={item.agentId}
|
||
data-tone={item.tone} style={{ '--welcome-delay': `${Math.min(index, 4) * 180}ms` } as CSSProperties}>
|
||
<button type="button" className="teacher-welcome-avatar" aria-label={`认识${item.definition.name}`} onClick={() => meetTeacher(item.agentId)}>
|
||
<TeacherAvatar definition={item.definition} />
|
||
</button>
|
||
<div className="teacher-welcome-bubble">
|
||
<button type="button" className="teacher-welcome-open" aria-label={`和${item.definition.name}聊聊`} onClick={() => meetTeacher(item.agentId)}>
|
||
<span className="teacher-welcome-name">{item.definition.name}</span>
|
||
<span className="teacher-welcome-text" title={item.text}>{item.text}</span>
|
||
<span className="teacher-welcome-action">和我聊聊 <ArrowUpRight aria-hidden="true" className="h-3 w-3" /></span>
|
||
</button>
|
||
<button type="button" className="teacher-welcome-dismiss" aria-label={`收起全部老师介绍(${item.definition.name})`} onClick={() => companion.dismissWelcomes()}>
|
||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
</article>)}
|
||
</div>
|
||
</div>}
|
||
{(invitation || welcome) && <div ref={bubbleRef} className="teacher-companion-presence" data-testid="teacher-presence">
|
||
<div className="teacher-companion-bubble" data-testid="teacher-invitation" data-bubble-kind={invitation ? 'check-in' : 'welcome'}>
|
||
<div className="teacher-companion-bubble-heading">
|
||
<div className="teacher-companion-invitation-label">{name}</div>
|
||
<button type="button" className="teacher-companion-dismiss" aria-label="收起智能体气泡" onClick={companion.dismiss}><X className="h-3.5 w-3.5" aria-hidden="true" /></button>
|
||
</div>
|
||
<p className="teacher-companion-message" role="status">{invitation?.response ?? welcome}</p>
|
||
<div className="teacher-companion-actions">
|
||
<button type="button" onClick={viewConversation}>{open ? '看看智能体说的' : '和智能体聊聊'} <ArrowUpRight aria-hidden="true" className="h-3 w-3" /></button>
|
||
<button type="button" onClick={companion.dismiss}>等会儿聊</button>
|
||
</div>
|
||
</div>
|
||
<button type="button" className="teacher-companion-speaker" aria-label="打开这条智能体消息" onMouseDown={(event) => event.preventDefault()} onClick={viewConversation}>
|
||
<TeacherAvatar definition={definition} />
|
||
</button>
|
||
</div>}
|
||
</section>
|
||
</TooltipProvider>
|
||
);
|
||
}
|