feat(teacher): show a subtle hint action in the empty composer

This commit is contained in:
鲨鱼辣椒
2026-09-28 15:17:18 +08:00
parent e00b3912bc
commit 73331e64f2
8 changed files with 184 additions and 62 deletions

View File

@@ -316,11 +316,14 @@ export function TeacherChatPanel({
const previous = pending.current && !current.requests.some((request) => request.id === pending.current?.requestId)
? pending.current
: null;
// Copy changes must not create another charge for an uncertain hint request.
const retrySuggestions = action?.intent === 'suggestions' && previous?.intent === 'suggestions';
const input =
previous &&
previous.text === question &&
(previous.intent ?? 'question') === (action?.intent ?? 'question') &&
JSON.stringify(previous.references) === JSON.stringify(questionReferences)
previous && (retrySuggestions || (
previous.text === question &&
(previous.intent ?? 'question') === (action?.intent ?? 'question') &&
JSON.stringify(previous.references) === JSON.stringify(questionReferences)
))
? previous
: {
requestId: crypto.randomUUID(), text: question, references: questionReferences,
@@ -352,8 +355,7 @@ export function TeacherChatPanel({
(request) => request.status === 'preparing' || request.status === 'running'
);
const helpUnavailable = busy || !topicEnabled || Boolean(running);
const askForSuggestions = () => void send({ text: '帮我看看', intent: 'suggestions' });
const askForGuidance = () => void send({ text: '我也说不清,你带我看看', intent: 'guided-help' });
const askForSuggestions = () => void send({ text: '给我一点提示', intent: 'suggestions' });
const changeText = (next: string) => { setText(next); persistDraft(next, references, pending.current); };
const removeReferences = () => { setReferences([]); persistDraft(text, [], pending.current); };
const questions = definition?.suggested_questions ?? [];
@@ -401,9 +403,8 @@ export function TeacherChatPanel({
<div className="consultation-follow-ups" aria-label="可以和智能体聊的问题">
{request.suggestedQuestions?.map((question, index) => <button key={`${request.id}:${index}`} type="button" disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
</div>
<button type="button" disabled={helpUnavailable} onClick={askForGuidance} className="consultation-guided-help min-h-10 text-left text-xs leading-6 underline decoration-current/30 underline-offset-4 disabled:cursor-not-allowed disabled:opacity-50">我也说不清,你带我看看</button>
</>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-guided-help min-h-10 text-xs underline underline-offset-4 disabled:opacity-50">再请智能体看看</button>}
{['failed', 'cancelled', 'interrupted'].includes(request.status) && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-retry min-h-10 text-xs underline underline-offset-4 disabled:opacity-50">再请智能体看看</button>}
</> : request.response ? <TeacherReply text={request.response} /> : <div className="text-[13px] leading-7">{['running', 'preparing'].includes(request.status) ? <span role="status" className="inline-flex items-center gap-2 text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />{label}正在想…</span> : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : ''}</div>}
{request.intent !== 'suggestions' && request.status === 'completed' && Boolean(request.suggestedQuestions?.length) && <div className="consultation-follow-ups" aria-label="接着聊">
{request.suggestedQuestions?.map((question, index) => <button type="button" key={`${request.id}:${index}`} disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-follow-up"><span className="min-w-0 flex-1">{question}</span><ChevronRight className="h-4 w-4 shrink-0 opacity-70" aria-hidden="true" /></button>)}
@@ -443,9 +444,10 @@ export function TeacherChatPanel({
<textarea ref={inputRef} aria-label={`向${label}提问`} value={text} disabled={busy} maxLength={6000} onChange={(e) => changeText(e.target.value)} placeholder="和老师聊聊" rows={2}
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); void send(); } }}
className="min-h-[60px] w-full resize-none border-0 bg-transparent text-[13px] leading-6 outline-none placeholder:text-muted-foreground/65" />
<div className="mt-2 flex items-center justify-between gap-2" data-testid="consultation-composer-actions">
{!draftRevision ? <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-help-prompt flex min-h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-xs transition-colors disabled:cursor-not-allowed disabled:opacity-50">帮我看看<ChevronRight className="h-3 w-3" aria-hidden="true" /></button>
: <span />}
<div className="mt-2 flex min-h-9 items-center justify-between gap-2" data-testid="consultation-composer-actions">
<div className="min-w-0 flex-1">
{!draftRevision && !text.trim() && <button type="button" disabled={helpUnavailable} onClick={askForSuggestions} className="consultation-hint flex min-h-9 cursor-pointer items-center gap-1 rounded-md px-1 text-[13px] transition-colors disabled:cursor-not-allowed disabled:opacity-50">给我一点提示<ChevronRight className="h-3.5 w-3.5" aria-hidden="true" /></button>}
</div>
{running ? <button type="button" className="rounded-lg border px-3 py-2 text-xs" onClick={() => void teacherApi.cancel(topicBase, topic!.id, running.id).then(consume).catch(() => setError('暂时无法停止,请稍后重试。'))}>停止回复</button>
: <button type="submit" aria-label="提问" title={busy ? '正在发送…' : '发送'} disabled={busy || !topicEnabled || !text.trim()} className="consultation-send flex h-8 w-8 items-center justify-center rounded-full text-white disabled:opacity-30"><ArrowUp className="h-4 w-4" /></button>}
</div>

View File

@@ -49,9 +49,9 @@
.classroom-consultation-resizer { position: absolute; inset: 0 auto 0 -4px; width: 8px; z-index: 2; cursor: col-resize; }
.classroom-consultation-resizer:hover, .classroom-consultation-resizer:focus-visible { background: #7994b533; }
.consultation-pane { background: #fffdf8; color: #30465f; }
.consultation-help-prompt, .consultation-guided-help { color: #536747; }
.consultation-help-prompt { background: #f1f5ea; border: 1px solid #dfe7d5; }
.consultation-help-prompt:hover:not(:disabled) { background: #e6eedc; border-color: #bacba8; }
.consultation-hint { color: #5c7087; background: transparent; border: 0; text-underline-offset: 4px; text-decoration: underline transparent; }
.consultation-hint:hover:not(:disabled) { color: #30465f; text-decoration-color: #a9bbce; }
.consultation-retry { color: #536747; }
.consultation-pane button:focus-visible { outline: 2px solid #71845f; outline-offset: 3px; }
.consultation-suggestion { border-color: #e0e6d6; color: #718163; }
.consultation-suggestion:hover { border-color: #bac9aa; background: #f4f7ee; }