Files
makelore/src/pages/Chat/TeacherCompanion.tsx
2026-09-30 12:49:57 +08:00

103 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}