Merge preview toolbar and icon-only teacher refresh

This commit is contained in:
鲨鱼辣椒 committed 2026-09-30 12:55:41 +08:00
commit 0a6ab29e9f
11 files changed
+334 -36

No files matched your search

+35 -20
View File
@@ -6,6 +6,7 @@ import {
useState,
type FormEvent,
} from 'react';
import { createPortal } from 'react-dom';
import {
ArrowLeft,
ArrowRight,
@@ -46,6 +47,7 @@ export interface AgentBrowserPanelProps {
open: boolean;
onOpenChange(open: boolean): void;
embedded?: boolean;
toolbarContainer?: HTMLElement | null;
teacherBubble?: HTMLElement | null;
teacherIntroduction?: HTMLElement | null;
conversationId?: string | null;
@@ -133,6 +135,7 @@ export function AgentBrowserPanel({
open,
onOpenChange,
embedded = false,
toolbarContainer,
teacherBubble,
teacherIntroduction,
conversationId,
@@ -482,6 +485,37 @@ export function AgentBrowserPanel({
if (!open) return null;
const toolbar = (
<form
data-testid="agent-browser-toolbar"
aria-label="网页导航"
className={cn(
'flex shrink-0 items-center gap-1',
embedded && toolbarContainer ? 'h-8 w-full min-w-0' : 'h-10 border-b border-border/70 px-2',
)}
onSubmit={handleOpenAddress}
>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 rounded-md" aria-label="后退" disabled={!snapshot?.canGoBack || busy} onClick={() => handleNavigate('back')}>
<ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 rounded-md" aria-label="前进" disabled={!snapshot?.canGoForward || busy} onClick={() => handleNavigate('forward')}>
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0 rounded-md" aria-label="刷新网页" disabled={!browserActive || busy} onClick={() => handleNavigate('reload')}>
<RefreshCw className={cn('h-3.5 w-3.5', busy && 'animate-spin')} aria-hidden="true" />
</Button>
<input
ref={addressInputRef}
value={address}
onChange={(event) => setAddress(event.target.value)}
aria-label="网页地址"
className="h-7 min-w-0 flex-1 rounded-lg border border-border/90 bg-surface-input px-2.5 text-[11px] font-medium outline-none focus:border-brand/40 focus:ring-2 focus:ring-brand/15"
placeholder={embedded ? '作品打开后会显示地址' : '输入 URL'}
spellCheck={false}
/>
</form>
);
return (
<aside
ref={panelRef}
@@ -506,26 +540,7 @@ export function AgentBrowserPanel({
</Button>
</div>}
<form className="flex h-10 shrink-0 items-center gap-1 border-b border-border/70 px-2" onSubmit={handleOpenAddress}>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 rounded-md" aria-label="后退" disabled={!snapshot?.canGoBack || busy} onClick={() => handleNavigate('back')}>
<ArrowLeft className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 rounded-md" aria-label="前进" disabled={!snapshot?.canGoForward || busy} onClick={() => handleNavigate('forward')}>
<ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 rounded-md" aria-label="刷新网页" disabled={!browserActive || busy} onClick={() => handleNavigate('reload')}>
<RefreshCw className={cn('h-3.5 w-3.5', busy && 'animate-spin')} aria-hidden="true" />
</Button>
<input
ref={addressInputRef}
value={address}
onChange={(event) => setAddress(event.target.value)}
aria-label="网页地址"
className="h-7 min-w-0 flex-1 rounded-lg border border-border/90 bg-surface-input px-2.5 text-[11px] font-medium outline-none focus:border-brand/40 focus:ring-2 focus:ring-brand/15"
placeholder={embedded ? '作品打开后会显示地址' : '输入 URL'}
spellCheck={false}
/>
</form>
{embedded && toolbarContainer ? createPortal(toolbar, toolbarContainer) : toolbar}
{error && !workEmpty ? (
<p role="alert" className="shrink-0 border-b border-destructive/15 bg-destructive/5 px-3 py-2 text-xs text-destructive">
+8 -3
View File
@@ -183,6 +183,7 @@ export function CodingChatPanel({
Record<string, LocalComposerAttachment[]>
>({});
const [agentBrowserOpen, setAgentBrowserOpen] = useState(false);
const [browserToolbar, setBrowserToolbar] = useState<HTMLDivElement | null>(null);
const [workRevision, setWorkRevision] = useState(0);
const [readyWorkProjectId, setReadyWorkProjectId] = useState<string | null>(null);
const recoveryFlight = useRef(false);
@@ -796,12 +797,16 @@ export function CodingChatPanel({
teacherEntry={activeProject ? <TeacherCompanion companion={teacherCompanion} open={consultationOpen} onOpen={openTeacher} onToggle={toggleTeacher} bubbleRef={setTeacherBubble} introductionRef={setTeacherIntroduction} /> : undefined}
/>
<div className="classroom-tabs" role="tablist" aria-label="当前工作">
<button role="tab" id="classroom-chat-tab" aria-controls="classroom-chat-view" aria-selected={!agentBrowserOpen} tabIndex={!agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(false)} onKeyDown={(event) => { if (event.key === 'ArrowRight') { setAgentBrowserOpen(true); document.getElementById('classroom-work-tab')?.focus(); } }}><MessageSquare className="h-3.5 w-3.5" />操作对话</button>
<button role="tab" id="classroom-work-tab" aria-controls="classroom-work-view" aria-selected={agentBrowserOpen} tabIndex={agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(true)} onKeyDown={(event) => { if (event.key === 'ArrowLeft') { setAgentBrowserOpen(false); document.getElementById('classroom-chat-tab')?.focus(); } }}><Play className="h-3.5 w-3.5" />作品</button>
<div className="classroom-workspace-toolbar" data-testid="classroom-workspace-toolbar">
<div className="classroom-tabs" role="tablist" aria-label="当前工作">
<button role="tab" id="classroom-chat-tab" aria-controls="classroom-chat-view" aria-selected={!agentBrowserOpen} tabIndex={!agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(false)} onKeyDown={(event) => { if (event.key === 'ArrowRight') { setAgentBrowserOpen(true); document.getElementById('classroom-work-tab')?.focus(); } }}><MessageSquare className="h-3.5 w-3.5" />操作对话</button>
<button role="tab" id="classroom-work-tab" aria-controls="classroom-work-view" aria-selected={agentBrowserOpen} tabIndex={agentBrowserOpen ? 0 : -1} onClick={() => setAgentBrowserOpen(true)} onKeyDown={(event) => { if (event.key === 'ArrowLeft') { setAgentBrowserOpen(false); document.getElementById('classroom-chat-tab')?.focus(); } }}><Play className="h-3.5 w-3.5" />作品</button>
</div>
<div className="classroom-browser-toolbar" ref={setBrowserToolbar} hidden={!agentBrowserOpen} />
</div>
<div id="classroom-work-view" role="tabpanel" aria-labelledby="classroom-work-tab" hidden={!agentBrowserOpen} className="classroom-work-view min-h-0 flex-1 flex-col p-5">
<AgentBrowserPanel projectId={activeProject?.id ?? null} open={agentBrowserOpen} onOpenChange={setAgentBrowserOpen} embedded
toolbarContainer={browserToolbar}
teacherBubble={teacherBubble}
teacherIntroduction={teacherIntroduction}
workRevision={workRevision}
-1
View File
@@ -58,7 +58,6 @@ export function TeacherCompanion({ companion, open, onOpen, onToggle, bubbleRef,
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" />
<span>{companion.refreshing ? '刷新中' : companion.refreshStatus === 'error' ? '刷新失败' : companion.refreshStatus === 'success' ? '已更新' : '刷新'}</span>
</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="认识这里的老师">
+5 -2
View File
@@ -40,6 +40,9 @@
[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-member-menu-trigger"] { min-height: 42px; border: 1px solid #d9e3ee; border-radius: 11px; background: #fff; font-size: 12px; }
.classroom-workspace [data-testid="coding-conversation-header"] { border-bottom-color: #e8edf3; background: #fff; color: #304968; }
.classroom-tabs { display: flex; flex-shrink: 0; height: 46px; gap: 9px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
.classroom-workspace-toolbar { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; gap: 6px 12px; min-height: 46px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; }
.classroom-workspace-toolbar > .classroom-tabs { height: 32px; padding: 0; border-bottom: 0; }
.classroom-browser-toolbar { flex: 1 1 220px; min-width: min(220px, 100%); }
.classroom-tabs button { display: flex; align-items: center; gap: 7px; position: relative; padding: 0 12px; border: 1px solid transparent; border-radius: 9px; background: #f4f7fa; font-size: 12px; color: #62738b; }
.classroom-tabs button:hover { background: #eaf1f8; }
.classroom-tabs button[aria-selected="true"] { background: #eaf1fb; border-color: #ccdaeb; color: #36557b; box-shadow: 0 2px 0 #dce6f3; }
@@ -86,7 +89,7 @@
.consultation-user { background: #eaf0e1; }
.consultation-composer { border-color: #dce3d3; border-bottom-width: 3px; }
.consultation-send { background: #536947; }
@media (max-width: 1100px) { .classroom-tabs { padding-left: 18px; padding-right: 18px; } }
@media (max-width: 1100px) { .classroom-tabs, .classroom-workspace-toolbar { padding-left: 18px; padding-right: 18px; } .classroom-workspace-toolbar > .classroom-tabs { padding: 0; } }
@media (max-width: 900px) { .classroom-consultation-dock { position: absolute; inset: 0 0 0 auto; z-index: 45; max-width: 100%; box-shadow: -12px 0 36px #24311b0d; } .classroom-consultation-resizer { display: none; } }
@media (max-width: 600px) { .classroom-consultation-dock { width: 100%!important; } .classroom-workspace [data-testid="coding-message-composer"] { width: calc(100% - 24px); } }
@media (prefers-reduced-motion: reduce) { .classroom-workspace *, .consultation-pane * { scroll-behavior: auto!important; } }
@@ -107,7 +110,7 @@
@keyframes teacher-introduction-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .teacher-introduction { animation: none; } }
.consultation-agent-tabs { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 100%; overflow-x: auto; scrollbar-width: thin; }
.consultation-agents-refresh { display: flex; align-items: center; justify-content: center; gap: 5px; flex-shrink: 0; width: 76px; height: 32px; border: 1px solid #dce4d5; border-radius: 8px; color: #526348; background: #f7f9f3; font-size: 12px; white-space: nowrap; cursor: pointer; transition: background 150ms; }
.consultation-agents-refresh { display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 32px; height: 32px; border: 1px solid #dce4d5; border-radius: 8px; color: #526348; background: #f7f9f3; cursor: pointer; transition: background 150ms; }
.consultation-agents-refresh:hover:not(:disabled) { background: #eaf0e1; }
.consultation-agents-refresh:disabled { cursor: default; opacity: .6; }
.consultation-agent-tabs .teacher-companion-person { flex-shrink: 0; }