feat: integrate pinned teacher discussion workspace
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Cpu,
|
||||
ChevronRight,
|
||||
FolderKanban,
|
||||
Gamepad2,
|
||||
LogOut,
|
||||
Plus,
|
||||
RadioTower,
|
||||
@@ -476,7 +477,7 @@ export function Sidebar({
|
||||
: 'bg-transparent text-foreground hover:bg-surface-tertiary',
|
||||
)}
|
||||
>
|
||||
<FolderKanban className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /><span className="min-w-0 flex-1 truncate text-[13px] font-medium">{project.name}</span>
|
||||
<Gamepad2 className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /><span className="min-w-0 flex-1 truncate text-[13px] font-medium">{project.name}</span>
|
||||
<ChevronRight aria-hidden="true" className="h-3.5 w-3.5 shrink-0 -translate-x-0.5 text-muted-foreground opacity-50 transition-[opacity,transform,color] duration-200 ease-out group-hover/project:translate-x-0 group-hover/project:text-foreground group-hover/project:opacity-100 group-focus-visible/project:translate-x-0 group-focus-visible/project:text-foreground group-focus-visible/project:opacity-100" />
|
||||
</button>
|
||||
{active && <CodingProjectConversations key={project.id} projectId={project.id} compact={workspaceLayout} />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
TeacherCheckInInput,
|
||||
TeacherCheckInResult,
|
||||
TeacherDefinition,
|
||||
TeacherDiscussionAction,
|
||||
TeacherSend,
|
||||
TeacherTopic,
|
||||
TeacherTopicList,
|
||||
@@ -42,6 +43,10 @@ export const teacherApi = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
}),
|
||||
updateDiscussion: (base: string, id: string, input: TeacherDiscussionAction) =>
|
||||
hostApiFetch<TeacherTopic>(base + '/' + encodeURIComponent(id) + '/discussion', {
|
||||
method: 'POST', body: JSON.stringify(input),
|
||||
}),
|
||||
cancel: (base: string, id: string, requestId: string) =>
|
||||
hostApiFetch<TeacherTopic>(
|
||||
base +
|
||||
|
||||
@@ -13,6 +13,8 @@ import type {
|
||||
TeacherTopic,
|
||||
TeacherTopicList,
|
||||
} from '../../../shared/coding-teacher';
|
||||
import { TeacherDiscussionPanel, DiscussionFocus } from './TeacherDiscussionPanel';
|
||||
import type { TeacherDiscussionAction } from '../../../shared/coding-teacher';
|
||||
import { getAgentAvatarSrc } from '@/lib/agent-avatars';
|
||||
|
||||
export function TeacherChatPanel({
|
||||
@@ -58,6 +60,7 @@ export function TeacherChatPanel({
|
||||
const label = role === 'friend' ? '朋友' : '老师';
|
||||
const studentTeacher = role === 'teacher' && !draftRevision;
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [focus, setFocus] = useState<{ topicId: string; id: string; label: string } | null>(null);
|
||||
const [definition, setDefinition] = useState<TeacherDefinition | null>(null);
|
||||
const avatar = role === 'friend' ? friendAvatar : draftRevision && definition ? getAgentAvatarSrc(definition.avatar_id) : teacherAvatar;
|
||||
const externalTopicRef = useRef(externalTopic);
|
||||
@@ -83,6 +86,12 @@ export function TeacherChatPanel({
|
||||
const topicRef = useRef(topic);
|
||||
topicRef.current = topic;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const content = topic?.discussion?.content;
|
||||
const activeFocus = focus && focus.topicId === topic?.id && content && topic?.discussion?.status === 'active' && (
|
||||
content.kind === 'ideas' ? content.items.some(item => item.id === focus.id)
|
||||
: content.kind === 'comparison' ? content.rows.some(row => content.columns.some(column => `${row.id}:${column.id}` === focus.id))
|
||||
: content.nodes.some(node => node.id === focus.id)
|
||||
) ? focus : null;
|
||||
useEffect(() => {
|
||||
const pane = scrollRef.current;
|
||||
if (pane) pane.scrollTop = pane.scrollHeight;
|
||||
@@ -232,7 +241,7 @@ export function TeacherChatPanel({
|
||||
if (version === generation.current) setBusy(false);
|
||||
}
|
||||
};
|
||||
const send = async (action?: Pick<TeacherSend, 'text' | 'intent'>) => {
|
||||
const send = async (action?: Pick<TeacherSend, 'text' | 'intent' | 'discussion'>) => {
|
||||
const question = action?.text ?? text;
|
||||
const questionReferences = action ? [] : references;
|
||||
if (!question.trim() || busy || !enabled || topicRef.current?.requests.some((request) => ['preparing', 'running'].includes(request.status))) return;
|
||||
@@ -246,17 +255,24 @@ export function TeacherChatPanel({
|
||||
if (version !== generation.current) return;
|
||||
consume(current);
|
||||
}
|
||||
const presentation = studentTeacher && (!action?.intent || action.intent === 'question' || action.intent === 'guided-help') ? 'discussion-v1' as const : undefined;
|
||||
const discussion = presentation && current.discussion?.status === 'active'
|
||||
? action?.discussion ?? { toolId: current.discussion.id, revision: current.discussion.revision,
|
||||
...(activeFocus?.topicId === current.id ? { focusId: activeFocus.id } : {}) } : undefined;
|
||||
const previous = pending.current && !current.requests.some((request) => request.id === pending.current?.requestId)
|
||||
? pending.current
|
||||
: null;
|
||||
const input =
|
||||
previous &&
|
||||
previous.text === question &&
|
||||
previous.presentation === presentation &&
|
||||
JSON.stringify(previous.discussion) === JSON.stringify(discussion) &&
|
||||
(previous.intent ?? 'question') === (action?.intent ?? 'question') &&
|
||||
JSON.stringify(previous.references) === JSON.stringify(questionReferences)
|
||||
? previous
|
||||
: {
|
||||
requestId: crypto.randomUUID(), text: question, references: questionReferences,
|
||||
...(presentation ? { presentation } : {}), ...(discussion ? { discussion } : {}),
|
||||
...(action?.intent ? { intent: action.intent } : {}),
|
||||
...(role && sourceId ? { sourceConversationId: sourceId } : {}),
|
||||
};
|
||||
@@ -285,6 +301,33 @@ export function TeacherChatPanel({
|
||||
(request) => request.status === 'preparing' || request.status === 'running'
|
||||
);
|
||||
const helpUnavailable = busy || !enabled || Boolean(running);
|
||||
const updateDiscussion = async (action: TeacherDiscussionAction['action'], itemId?: string) => {
|
||||
const current = topicRef.current;
|
||||
if (!current?.discussion || helpUnavailable) return;
|
||||
const version = generation.current;
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const next = await teacherApi.updateDiscussion(base, current.id, {
|
||||
toolId: current.discussion.id, revision: current.discussion.revision, action, ...(itemId ? { itemId } : {}),
|
||||
});
|
||||
if (version === generation.current) { consume(next); setFocus(null); }
|
||||
} catch (e) {
|
||||
if (version === generation.current) setError(e instanceof Error ? e.message : '内容还没更新,请再试一次。');
|
||||
} finally { if (version === generation.current) setBusy(false); }
|
||||
};
|
||||
const focusItem = (id: string, label: string) => {
|
||||
if (!topic) return;
|
||||
setFocus({ topicId: topic.id, id, label });
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
const structureIdeas = () => {
|
||||
if (!topic?.discussion) return;
|
||||
setFocus(null);
|
||||
void send({ text: '把我留下的想法理一理,看看它们怎么联系。', discussion: {
|
||||
toolId: topic.discussion.id, revision: topic.discussion.revision, transition: 'structure',
|
||||
} });
|
||||
};
|
||||
const askForSuggestions = () => void send({ text: '老师帮我看看', intent: 'suggestions' });
|
||||
const askForGuidance = () => void send({ text: '我也说不清,你带我看看', intent: 'guided-help' });
|
||||
const changeText = (next: string) => { setText(next); persistDraft(next, references, pending.current); };
|
||||
@@ -310,6 +353,13 @@ export function TeacherChatPanel({
|
||||
</select><button aria-label={`${label}新话题`} title="新话题" disabled={busy || !enabled} onClick={() => void newTopic()} className="consult-icon"><Plus className="h-4 w-4" /></button>
|
||||
</div>}
|
||||
</div>}
|
||||
{studentTeacher && topic?.discussion && <TeacherDiscussionPanel key={topic.id} discussion={topic.discussion}
|
||||
disabled={helpUnavailable} focusId={activeFocus?.id}
|
||||
onAction={(action, itemId) => void updateDiscussion(action, itemId)} onFocus={focusItem} onStructure={structureIdeas}
|
||||
onDiscuss={(id, question) => {
|
||||
focusItem(id, question);
|
||||
void send({ text: question, discussion: { toolId: topic.discussion!.id, revision: topic.discussion!.revision, focusId: id } });
|
||||
}} />}
|
||||
<div ref={scrollRef} className="consultation-messages min-h-0 flex-1 overflow-y-auto px-5 py-6">
|
||||
{!topic?.requests.length && <div className="flex min-h-full flex-col items-center justify-center pb-5 text-center">
|
||||
<img className="mb-6 h-[68px] w-[68px] rounded-3xl [image-rendering:pixelated]" src={avatar} alt="" />
|
||||
@@ -337,7 +387,11 @@ export function TeacherChatPanel({
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">也可以直接在下面说说你的想法。</p>
|
||||
</>}
|
||||
{['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>}
|
||||
</> : <div className="whitespace-pre-wrap break-words text-[13px] leading-7">{request.response || (['running', 'preparing'].includes(request.status) ? `${label}正在想…` : '')}</div>}
|
||||
</> : <div className="whitespace-pre-wrap break-words text-[13px] leading-7">{request.response || (['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" />{request.discussionContext ? '正在梳理你的新想法…' : `${label}正在想…`}</span> : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : '')}</div>}
|
||||
{studentTeacher && request.intent !== 'suggestions' && request.status === 'completed' && topic.discussion?.status !== 'active' && topic.discussion?.status !== 'offered' && Boolean(request.suggestedQuestions?.length) && <div className="flex flex-col gap-2" aria-label="接着聊">
|
||||
{request.suggestedQuestions?.map(question => <button type="button" key={question} disabled={helpUnavailable} onClick={() => void send({ text: question })} className="consultation-suggestion rounded-xl border px-3 py-2 text-left text-xs leading-6">{question}<ChevronRight className="ml-1 inline h-3 w-3" /></button>)}
|
||||
</div>}
|
||||
{request.discussionError && <p className="text-xs leading-6 text-muted-foreground">{request.discussionError}</p>}
|
||||
{request.omittedMessages > 0 && <p className="text-[10px] text-muted-foreground">本轮参考了较近的讨论,省略了 {request.omittedMessages} 条较早内容。</p>}
|
||||
{request.error && <p className="text-xs text-destructive">{request.error}</p>}
|
||||
{!role && onBringBack && request.response && request.intent !== 'suggestions' && request.status === 'completed' && <div className="text-[11px] text-muted-foreground">
|
||||
@@ -351,6 +405,7 @@ export function TeacherChatPanel({
|
||||
<form className="px-4 pb-4 pt-2" onSubmit={(e) => { e.preventDefault(); void send(); }}>
|
||||
{references.length > 0 && <div className="mb-2 rounded-lg bg-white p-3 text-xs"><p className="line-clamp-3 whitespace-pre-wrap">{references.map((ref) => ref.text).join('\n')}</p><button type="button" className="mt-1 underline" onClick={removeReferences}>移除引用</button></div>}
|
||||
<div className="consultation-composer rounded-2xl border bg-white p-3 focus-within:ring-2 focus-within:ring-black/5">
|
||||
{activeFocus && <DiscussionFocus label={activeFocus.label} onClear={() => setFocus(null)} />}
|
||||
<textarea ref={inputRef} aria-label={`向${label}提问`} value={text} disabled={busy} maxLength={6000} onChange={(e) => changeText(e.target.value)} placeholder={`想和${label}聊些什么?`} 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" />
|
||||
|
||||
94
src/pages/Chat/TeacherDiscussionPanel.tsx
Normal file
94
src/pages/Chat/TeacherDiscussionPanel.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
import { ArrowDown, ArrowRight, Check, ChevronDown, ChevronUp, GitBranch, Layers3, MessageCircle, Puzzle, Sparkles, X } from 'lucide-react';
|
||||
import type { TeacherDiscussionAction } from '../../../shared/coding-teacher';
|
||||
import type { TeacherDiscussion, TeacherStructureNode } from '../../../shared/teacher-discussion';
|
||||
import './teacher-discussion.css';
|
||||
|
||||
type Action = TeacherDiscussionAction['action'];
|
||||
export function TeacherDiscussionPanel({ discussion, disabled, focusId, onAction, onFocus, onDiscuss, onStructure }: {
|
||||
discussion: TeacherDiscussion;
|
||||
disabled: boolean;
|
||||
focusId?: string;
|
||||
onAction(action: Action, itemId?: string): void;
|
||||
onFocus(id: string, label: string): void;
|
||||
onDiscuss(id: string, text: string): void;
|
||||
onStructure(): void;
|
||||
}) {
|
||||
const [folded, setFolded] = useState(false);
|
||||
const content = discussion.content;
|
||||
const active = discussion.status === 'active';
|
||||
const offered = discussion.status === 'offered';
|
||||
const collapsed = folded || (!active && !offered);
|
||||
const Icon = content.kind === 'ideas' ? Puzzle : content.kind === 'structure' ? Layers3 : content.kind === 'flow' ? GitBranch : Layers3;
|
||||
const select = (id: string, label: string) => { if (active && !disabled) onFocus(id, label); };
|
||||
const tree = (parentId?: string): React.ReactNode => content.kind === 'structure' && content.nodes.filter(node => node.parentId === parentId).map((node: TeacherStructureNode) => (
|
||||
<li key={node.id}>
|
||||
<button type="button" disabled={!active || disabled} aria-pressed={focusId === node.id} className="discussion-node" onClick={() => select(node.id, node.label)}>
|
||||
{node.relation && <span className="discussion-relation">{node.relation}</span>}<span>{node.label}</span>
|
||||
</button>
|
||||
{content.nodes.some(child => child.parentId === node.id) && <ul>{tree(node.id)}</ul>}
|
||||
</li>
|
||||
));
|
||||
return <section className="teacher-discussion" data-testid="teacher-discussion" data-kind={content.kind} data-collapsed={collapsed} aria-label="一起整理的内容">
|
||||
<header className="discussion-heading">
|
||||
<span className="discussion-emblem"><Icon size={17} aria-hidden="true" /></span>
|
||||
<div><span className="discussion-eyebrow">{offered ? '要不要一起理一理?' : active ? '我们正在一起想' : discussion.status === 'finished' ? '先留在这里' : '想法还在,慢慢聊'}</span><h3 title={content.title}>{content.title}</h3></div>
|
||||
{(active || offered) && <button type="button" className="discussion-icon" aria-label={collapsed ? '展开整理内容' : '收起整理内容'} aria-expanded={!collapsed} onClick={() => setFolded(!folded)}>{collapsed ? <ChevronDown size={16} /> : <ChevronUp size={16} />}</button>}
|
||||
{!active && !offered && <button type="button" className="discussion-resume" disabled={disabled} onClick={() => { setFolded(false); onAction('resume'); }}>接着改</button>}
|
||||
</header>
|
||||
{!collapsed && <>
|
||||
<div className="discussion-content" tabIndex={0} aria-label="整理内容,可滚动查看">
|
||||
{content.kind === 'ideas' && <div className="discussion-ideas">
|
||||
{(['kept', 'suggested', 'aside'] as const).map(state => {
|
||||
const items = content.items.filter(item => item.state === state);
|
||||
return items.length > 0 && <div key={state} className={`discussion-idea-group discussion-${state}`}>
|
||||
<p className="discussion-group-label">{state === 'kept' ? '我想留下的' : state === 'suggested' ? '老师想到的,你来决定' : '暂时放一边'}</p>
|
||||
{items.map(item => <div key={item.id} className="discussion-idea" data-selected={focusId === item.id}>
|
||||
{item.parentId && <span className="discussion-relation">关于「{content.items.find(parent => parent.id === item.parentId)?.text}」</span>}
|
||||
<button type="button" className="discussion-idea-text" disabled={!active || disabled} onClick={() => select(item.id, item.text)} aria-pressed={focusId === item.id}>
|
||||
<span className="discussion-piece"><Puzzle size={14} /></span><span>{item.text}</span>
|
||||
</button>
|
||||
{active && <div className="discussion-item-actions">
|
||||
{state !== 'kept' ? <button disabled={disabled} onClick={() => onAction('keep-idea', item.id)}><Check size={12} />留下</button> : <button aria-pressed={content.firstItemId === item.id} disabled={disabled} onClick={() => onAction('first-idea', item.id)}>{content.firstItemId === item.id ? <><Check size={12} />先想这个</> : '从这里开始'}</button>}
|
||||
{state !== 'aside' && <button disabled={disabled} onClick={() => onAction('aside-idea', item.id)}>先放一边</button>}
|
||||
</div>}
|
||||
</div>)}
|
||||
</div>;
|
||||
})}
|
||||
</div>}
|
||||
{content.kind === 'structure' && <ul className="discussion-tree">{tree()}</ul>}
|
||||
{content.kind === 'flow' && <div className="discussion-flow">
|
||||
{content.nodes.map((node, index) => <div key={node.id} className="discussion-flow-step">
|
||||
<button type="button" className="discussion-node" data-node-kind={node.kind} aria-pressed={focusId === node.id} disabled={!active || disabled} onClick={() => select(node.id, node.label)}>
|
||||
<span className="discussion-step-number">{index + 1}</span><span><small>{node.kind === 'event' ? '当这件事发生' : node.kind === 'condition' ? '看看是否满足' : '接下来'}</small>{node.label}</span>
|
||||
</button>
|
||||
{content.edges.filter(edge => edge.from === node.id).map(edge => <button type="button" key={edge.id} className="discussion-edge" disabled={!active || disabled} onClick={() => select(edge.to, content.nodes.find(next => next.id === edge.to)?.label ?? '')}>
|
||||
<ArrowDown size={12} /><span>{edge.label && <b>{edge.label} → </b>}{content.nodes.find(next => next.id === edge.to)?.label}</span><span className="discussion-destination">{content.nodes.findIndex(next => next.id === edge.to) + 1}</span>
|
||||
</button>)}
|
||||
</div>)}
|
||||
</div>}
|
||||
{content.kind === 'comparison' && <div className="discussion-comparison" role="table" aria-label={content.title}>
|
||||
{content.rows.map(row => <div className="discussion-comparison-row" key={row.id} role="rowgroup">
|
||||
<h4>{row.label}</h4><div className="discussion-comparison-cells" style={{ gridTemplateColumns: `repeat(${content.columns.length === 3 ? 3 : 2}, minmax(0, 1fr))` }} role="row">
|
||||
{content.columns.map(column => <div key={column.id} role="cell" className="discussion-comparison-cell" data-selected={focusId === `${row.id}:${column.id}`}>
|
||||
<b>{column.label}</b><p>{row.cells.find(cell => cell.columnId === column.id)?.text}</p>
|
||||
{active && <button type="button" disabled={disabled} onClick={() => onDiscuss(`${row.id}:${column.id}`, `聊聊「${row.label}」时,「${column.label}」这一点:${row.cells.find(cell => cell.columnId === column.id)?.text ?? ''}`)}><MessageCircle size={12} />聊聊这一点</button>}
|
||||
</div>)}
|
||||
</div>
|
||||
</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
<footer className="discussion-footer">
|
||||
{offered ? <><button type="button" className="discussion-primary" disabled={disabled} onClick={() => onAction('enter')}><Sparkles size={13} />用这个一起想</button><button type="button" disabled={disabled} onClick={() => onAction('pause')}>先不用</button></> : <>
|
||||
{content.kind === 'ideas' && <button type="button" className="discussion-next" disabled={disabled || !content.items.some(item => item.state === 'kept')} onClick={onStructure}>把想法理一理<ArrowRight size={13} /></button>}
|
||||
{content.kind === 'structure' && discussion.previousIdeas && <button type="button" className="discussion-next" disabled={disabled} onClick={() => onAction('back-ideas')}>回去补充想法</button>}
|
||||
<div className="discussion-footer-actions"><button type="button" disabled={disabled} onClick={() => onAction('pause')}>只聊天</button><button type="button" className="discussion-primary" disabled={disabled} onClick={() => onAction('finish')}><Check size={13} />先这些</button></div>
|
||||
</>}
|
||||
</footer>
|
||||
</>}
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function DiscussionFocus({ label, onClear }: { label: string; onClear(): void }) {
|
||||
return <div className="discussion-focus"><MessageCircle size={13} /><span>接着聊:{label}</span><button type="button" aria-label="取消讨论焦点" onClick={onClear}><X size={13} /></button></div>;
|
||||
}
|
||||
68
src/pages/Chat/teacher-discussion.css
Normal file
68
src/pages/Chat/teacher-discussion.css
Normal file
@@ -0,0 +1,68 @@
|
||||
.teacher-discussion { display:flex; flex-direction:column; flex-shrink:0; min-height:0; max-height:46%; margin:12px 12px 0; border:1px solid #d6e2cf; border-radius:16px; background:#fff; overflow:hidden; box-shadow:0 3px 0 #dce7d780; font-size:12px; color:#3b4934; }
|
||||
.teacher-discussion[data-kind="structure"] { border-color:#d6d7f1; }
|
||||
.teacher-discussion[data-kind="flow"] { border-color:#c8e3ee; }
|
||||
.teacher-discussion[data-kind="comparison"] { border-color:#efdcad; }
|
||||
.teacher-discussion button { cursor:pointer; transition:background 150ms, border-color 150ms; }
|
||||
.teacher-discussion button:disabled { cursor:default; opacity:.65; }
|
||||
.teacher-discussion button:focus-visible { outline:2px solid #607c45; outline-offset:-2px; }
|
||||
.discussion-heading { display:flex; align-items:center; gap:8px; flex-shrink:0; padding:11px 12px; background:#f1f6e9; }
|
||||
[data-kind="structure"] .discussion-heading { background:#f0eefa; }
|
||||
[data-kind="flow"] .discussion-heading { background:#eaf5fb; }
|
||||
[data-kind="comparison"] .discussion-heading { background:#fff5d9; }
|
||||
.discussion-heading>div { flex:1; min-width:0; }
|
||||
.discussion-heading h3 { margin:3px 0 0; font-weight:650; font-size:13px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; overflow-wrap:anywhere; line-height:1.5; }
|
||||
.discussion-eyebrow { color:#667158; font-size:10px; }
|
||||
.discussion-emblem { display:grid; place-items:center; flex-shrink:0; width:30px; height:30px; border:1px solid #d8e2ce; border-radius:9px; background:#fff; }
|
||||
.discussion-icon { display:grid; place-items:center; width:28px; height:32px; flex-shrink:0; }
|
||||
.discussion-content { overflow-y:auto; overflow-x:hidden; min-height:0; padding:12px; overscroll-behavior:contain; scrollbar-width:thin; }
|
||||
.discussion-group-label { font-size:10px; font-weight:600; color:#69715f; margin:0 0 7px; }
|
||||
.discussion-idea-group+.discussion-idea-group { margin-top:14px; }
|
||||
.discussion-idea { border:1px solid #dae5cd; border-radius:10px; background:#f4f8e9; padding:8px; margin-top:6px; }
|
||||
.discussion-suggested .discussion-idea { background:#fff6dd; border-color:#eedbaa; border-style:dashed; }
|
||||
.discussion-aside .discussion-idea { background:#f4f3f1; border-color:#e2e0d9; color:#6d7166; }
|
||||
.discussion-idea[data-selected="true"], .discussion-node[aria-pressed="true"], .discussion-comparison-cell[data-selected="true"] { outline:2px solid #768954; outline-offset:-2px; }
|
||||
.discussion-idea-text { display:flex; align-items:flex-start; gap:7px; text-align:left; width:100%; line-height:1.65; overflow-wrap:anywhere; }
|
||||
.discussion-piece { padding-top:3px; flex-shrink:0; color:#849464; }
|
||||
.discussion-item-actions { display:flex; justify-content:flex-end; gap:13px; flex-wrap:wrap; margin-top:7px; }
|
||||
.discussion-item-actions button { display:flex; align-items:center; gap:3px; min-height:28px; font-size:10px; color:#596546; }
|
||||
.discussion-item-actions button:hover:not(:disabled) { text-decoration:underline; }
|
||||
.discussion-item-actions button[aria-pressed="true"] { font-weight:700; }
|
||||
.discussion-relation { display:block; font-size:10px; color:#777a6b; margin:0 0 4px; overflow-wrap:anywhere; }
|
||||
.discussion-tree, .discussion-tree ul { list-style:none; padding:0; margin:0; }
|
||||
.discussion-tree ul { margin-left:10px; padding-left:10px; border-left:2px solid #dbd7ed; }
|
||||
.discussion-tree li { margin-top:7px; }
|
||||
.discussion-node { width:100%; border:1px solid #e1dcf2; border-radius:9px; background:#f5f2ff; padding:9px 10px; text-align:left; line-height:1.6; overflow-wrap:anywhere; }
|
||||
.discussion-node:hover:not(:disabled) { background:#ece8fa; }
|
||||
.discussion-flow-step+.discussion-flow-step { margin-top:10px; }
|
||||
.discussion-flow .discussion-node { display:flex; gap:8px; background:#eff8fb; border-color:#cbe3eb; }
|
||||
.discussion-flow .discussion-node[data-node-kind="condition"] { background:#fff7df; border-color:#efdcaa; }
|
||||
.discussion-flow .discussion-node[data-node-kind="outcome"] { background:#edf5e7; border-color:#d4e3c7; }
|
||||
.discussion-node small { display:block; font-size:9px; opacity:.8; }
|
||||
.discussion-step-number,.discussion-destination { display:grid; place-items:center; flex-shrink:0; width:20px; height:20px; border-radius:6px; background:#fff; border:1px solid #d6dfd0; font-size:10px; }
|
||||
.discussion-edge { display:flex; align-items:center; gap:5px; width:100%; padding:7px 4px 0 12px; text-align:left; font-size:10px; line-height:1.5; color:#627160; }
|
||||
.discussion-edge>span:first-of-type { flex:1; min-width:0; overflow-wrap:anywhere; }
|
||||
.discussion-edge svg { flex-shrink:0; }
|
||||
.discussion-edge b { font-weight:600; }
|
||||
.discussion-comparison-row+.discussion-comparison-row { margin-top:12px; }
|
||||
.discussion-comparison-row h4 { font-weight:600; font-size:11px; margin:0 0 7px; }
|
||||
.discussion-comparison-cells { display:grid; gap:6px; }
|
||||
.discussion-comparison-cell { min-width:0; display:flex; flex-direction:column; padding:9px 8px; background:#f4f2fc; border:1px solid #e1dcf1; border-radius:10px; overflow-wrap:anywhere; }
|
||||
.discussion-comparison-cell:nth-child(even) { background:#eef6e8; border-color:#dae6cc; }
|
||||
.discussion-comparison-cell>b { font-size:10px; }
|
||||
.discussion-comparison-cell p { flex:1; line-height:1.7; font-size:11px; margin:7px 0 8px; white-space:pre-wrap; }
|
||||
.discussion-comparison-cell button { display:flex; align-items:center; flex-wrap:wrap; gap:4px; font-size:10px; min-height:30px; color:#506144; text-align:left; }
|
||||
.discussion-footer { flex-shrink:0; padding:8px 11px; border-top:1px solid #eef0e8; display:flex; align-items:center; justify-content:space-between; gap:8px; flex-wrap:wrap; }
|
||||
.discussion-footer button,.discussion-resume { display:inline-flex; align-items:center; justify-content:center; gap:4px; min-height:32px; padding:5px 7px; font-size:11px; border-radius:8px; }
|
||||
.discussion-footer-actions { display:flex; align-items:center; gap:7px; justify-content:flex-end; width:100%; }
|
||||
.discussion-footer .discussion-primary { background:#526e3c; color:#fff; padding:6px 12px; }
|
||||
.discussion-footer .discussion-primary:hover:not(:disabled) { background:#405a2b; }
|
||||
.discussion-next { color:#5a6650; }
|
||||
.discussion-next:hover:not(:disabled), .discussion-resume:hover:not(:disabled) { background:#eaf0df; }
|
||||
.discussion-resume { flex-shrink:0; background:white; border:1px solid #dce3d1; }
|
||||
.discussion-focus { display:flex; align-items:center; gap:6px; padding:7px 9px; margin-bottom:6px; border:1px solid #dedfcd; background:#f4f5e8; border-radius:9px; font-size:11px; }
|
||||
.discussion-focus>span { min-width:0; flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
||||
.discussion-focus button { display:grid; place-items:center; width:26px; height:26px; cursor:pointer; }
|
||||
.consultation-pane>form { flex-shrink:0; }
|
||||
.teacher-discussion~.consultation-messages { padding-top:16px; padding-bottom:8px; }
|
||||
@media (max-height:650px) { .teacher-discussion { max-height:40%; } .discussion-heading { padding:7px 10px; } }
|
||||
@media (prefers-reduced-motion:reduce) { .teacher-discussion button { transition:none; } }
|
||||
Reference in New Issue
Block a user