From 317def79129503062d15076585881ceae753055c Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Mon, 28 Sep 2026 17:46:19 +0800 Subject: [PATCH] Remove teacher conversation discussion components --- .../20260928-remove-teacher-cards-ef1cf59c.md | 60 ++++++++ README.md | 9 +- src/pages/Chat/TeacherChatPanel.tsx | 72 ++-------- src/pages/Chat/TeacherDiscussionPanel.tsx | 94 ------------ src/pages/Chat/teacher-discussion.css | 68 --------- tests/e2e/fixtures/teacher-discussion-api.ts | 36 +---- tests/e2e/pi-coding-first-chat.spec.ts | 6 + tests/e2e/teacher-discussion-layout.spec.ts | 42 ++---- tests/unit/coding-teacher-ui.test.tsx | 134 +++++++----------- 9 files changed, 150 insertions(+), 371 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260928-remove-teacher-cards-ef1cf59c.md delete mode 100644 src/pages/Chat/TeacherDiscussionPanel.tsx delete mode 100644 src/pages/Chat/teacher-discussion.css diff --git a/.project-docs/30-worklog/tasks/20260928-remove-teacher-cards-ef1cf59c.md b/.project-docs/30-worklog/tasks/20260928-remove-teacher-cards-ef1cf59c.md new file mode 100644 index 00000000..ab2f7665 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260928-remove-teacher-cards-ef1cf59c.md @@ -0,0 +1,60 @@ +# Task: Remove teacher agent conversation component rendering + +## Identity + +- Task ID: 20260928-remove-teacher-cards-ef1cf59c +- Mode: Feature +- Branch: codex/20260928-remove-teacher-cards-ef1cf59c-remove-teacher-cards +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260928-remove-teacher-cards-ef1cf59c +- Base commit: 592920da060276fa222ce15383bc22d2dde2eb49 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Remove the screenshot's teacher consultation discussion components from MakeLore: ideas, structure, flow and comparison panels, their invitations, focus controls, actions and styles. +- Stop requesting component presentation for new ordinary questions and guided help. Preserve ordinary replies, quick suggestions, rich text, tool activity, project chat identity and historical data. +- Renderer, its focused unit/browser/Electron regressions and README only; no Main/backend protocol or storage migration. + +## Intent And Constraints + +- User explicitly requested removal of component rendering and prohibited subagents without consent. No subagents were created. +- Concurrent Task Gate passed: bundled check/start/status established this feature task, branch, absolute worktree and base with matching Git-common ownership. Primary main has unrelated untracked records and remained unchanged. +- Project Context Loaded: read-before-planning, own task, memory-index, project-positioning, current-state, decision-index, system-overview, business-rules, success-criteria and ADR-2026-09-22-coding-teacher; AGENTS.md and README supplement generic positioning placeholders. Current product is project-scoped ongoing agent chat; Main owns execution, history and read tools. +- Read all 132 peer task records' available Scope, Intent And Constraints and Promotion Candidates. Historical placeholder scopes remain unknown; related consultation/rendering work is integrated. Independent proactive observer work remains separate and does not require the removed component UI. No concrete semantic dependency blocks this task. +- Planning Gate Passed before edits. The explicit user request supersedes the old component-rendering behavior; canonical memory remains unchanged in feature mode. +- Original workspace was Works Square; matching visible labels were found only in MakeLore TeacherDiscussionPanel and TeacherChatPanel. Companion server task records that routing finding without product edits. +- An uncertain pre-upgrade request retries its original ID and full payload, including old presentation fields. New requests omit presentation/discussion. This preserves existing replay semantics and prevents duplicate model execution. + +## Outcome + +- Deleted TeacherDiscussionPanel.tsx and teacher-discussion.css, plus the panel mount, focus selection, state transitions and component API calls from TeacherChatPanel. +- Ordinary new questions and guided help use the existing non-component path. Old active/offered discussion state no longer hides suggested follow-up questions. +- Input stays fixed using shrink-0 after removal of its former CSS rule. Existing replies, failure originals, tool activity and all Main-owned history remain intact. +- Updated README and existing tests/fixtures to describe and verify the final behavior. +- No merge, push, packaged installation, deployment or paid model call performed. + +## Verification + +- pnpm 10.33.4 matches packageManager. Reused the primary checkout's existing dependencies through a local node_modules junction; no lockfile/dependency change. +- 47 tests passed: coding-teacher-ui.test.tsx and teacher-conversation-panel.test.tsx. Detects hidden old cards reappearing, new component request metadata, lost uncertain-request identity, broken cancellation/drafts and project conversation regressions. +- pnpm run typecheck passed; scoped ESLint passed for all changed TS/TSX files. +- pnpm run build:vite passed for Renderer/Main/Preload/utility. Existing bundle size, mixed-import and old Browserslist warnings remain. +- 12 Edge browser tests passed: 319px/508px ordinary and all four historical component kinds, plus Markdown/table/code/image/math layouts. Captured comparison screenshot was inspected: no component panel and composer remains visible. Vite reported KaTeX fonts outside its allow list because dependencies are reused across the worktree; mathematical DOM rendering passed, but this fixture run is not custom-font verification. +- 1 Electron scenario passed: project consultations preserve student drafts and switch between work/chat; ordinary/guided/suggestion messages have no new presentation/discussion metadata. +- Tests use deterministic API responses, not live provider calls. Task-aware documentation drift check passed. + +## Follow-ups + +- Integrate the client branch when requested; installed applications need an updated client build. +- Independent proactive-observer integration must retain this component-free consultation display. + +## Promotion Candidates + +- Target: coding-teacher ADR, system-overview and teacher behavior in current-state/business-rules. +- Proposal: replace the pinned discussion component UI contract with ordinary conversation; new questions and guided help omit discussion-v1, while existing structured histories and uncertain request recovery retain their original data. +- Evidence: explicit user screenshot request, Renderer deletion and passing unit/browser/Electron checks. +- Future impact: future consultation UI or observer work must not restore those components incidentally. +- Semantic conflict: intentionally supersedes the previously accepted visible component workflow; user has authorized this change. Canonical reconciliation belongs to a later Integration Gate, with no further product clarification required. + + diff --git a/README.md b/README.md index 28a2e999..07b75a11 100644 --- a/README.md +++ b/README.md @@ -175,14 +175,13 @@ Pi 正式包必须继续运行 `pnpm run verify:artifact:pi`、`pnpm run smoke:p - 新的单会话入口停止旧版前端定时跟进派发;项目主动观察由独立功能衔接,不能重新创建可见话题。未下发目录的旧版入口仍保留原跟进合同:前端每 5 分钟请求一次智能体跟进;窗口隐藏、来源归档、主对话正在执行、智能体正在回复或学生正在智能体栏写草稿时延后。Main 再检查在线启停、来源归属、项目级冷却和已完成文本指纹,未变上下文按下述十五分钟冷却处理。跟进沿用运营模型、已发布 Skills 与当前智能体话题,生成简短的具体建议或引导;以主动智能体发言持久化,不伪造学生消息。模型调用沿用智能体计费规则。 - 进入项目时,智能体头像旁先显示运营发布的欢迎语气泡,不调用模型、不声称已检查项目;每个账号/项目主动收起后不重复弹出。真实的未读主动建议优先替换欢迎语。主动发言在头像旁直接显示气泡,不抢焦点、不盖住作品;作品区按气泡与原生预览的实际交叠范围避让,已有工具栏高度计入计算,气泡位于独立智能体栏时不额外留白,窗口/分栏尺寸变化和收起气泡时实时归还空间;“和智能体聊聊”接回同一段咨询,“等会儿聊”收起并保留历史。已有未处理气泡时不堆叠新邀请;咨询已展开时也显示气泡,直到学生主动点击查看或收起,期间的其他回复不会吞掉未读邀请。五分钟检查计时在刷新和项目切换后保留;智能体咨询里的新讨论也算进展,即使操作对话没有变化也会继续跟进;上下文完全未变时放缓到十五分钟一次,不永久停止。事件连接丢失时,每十五秒只读同步未完成回复,收到结果后停止轮询,不重复调用模型。失败如实提示,不以本地文案替代模型回复。已接受请求恢复使用原身份,退出账号和来源删除仍沿用 Main 中止边界。 - 智能体同时支持自由提问与输入框底部的提示词按钮“帮我看看”:学生点击后才结合当前操作对话与咨询历史生成 2–3 个可聊的问题。点击生成的问题会主动开始讨论;“我也说不清,你带我看看”让智能体从一个具体、容易回答的交流起点带着聊。这些快捷求助保留已有输入草稿,网络结果不确定时复用请求身份,已确认终态的请求不重复执行;解析失败可重新求助,不用固定问题伪装模型结果。服务端下发的推荐问题只填入草稿,由用户确认发送。界面不提供独立示范页面、“记一下”、共识或自动待办,智能体的回复下不再展示“我去试一试”和“复制”按钮,学生通过已有的操作对话/作品入口继续创作。 -- 智能体讨论采用“上方固定整理内容、下方独立滚动对话、底部原有输入框”的布局。普通回答可带直接发送的引导问题;想法板、结构图、流程/条件图和逐项对照由同一次模型回复提供结构化数据。先邀请学生“用这个一起想”,进入后程序锁定信息结构,智能体随讨论更新同一份内容;解释问题可仅回复文字。节点点击只选择讨论焦点,对照里的“聊聊这一点”直接发问,都保留输入草稿。 -- 想法板区分已留下、智能体建议和暂放内容,可由学生采纳、暂放、选择先想哪项。“把想法理一理”明确转换为智能体归纳的结构图,并可“回去补充想法”。“只聊天”暂停整理,“先这些”暂时结束并保留未确定内容,“接着改”恢复原工具;这些操作只影响智能体讨论,不创建项目分支、不执行作品修改。当前讨论卡片按聊天中的项目分别保存,历史轮次保留卡片快照。 -- Main 校验结构、引用和内容长度,拒绝未经学生选择的类型变更与旧版本更新。结构化回复在完整校验后一次应用;生成中显示简短处理状态,停止、失败或无效组件保留上一份内容。普通 Markdown 链接、JSON 对象/数组和代码示例保持正文,仅讨论协议的顶层字段或专用围栏进入组件解析。解析失败仍保留完整的可读 reply,并将本次原始回答保存为默认折叠、字面显示的“查看收到的原始内容”,不把它再次加入模型上下文;既有丢失原文的历史不能恢复。 +- 智能体咨询栏保留普通消息与原有输入框,不再渲染想法板、结构图、流程/条件图、逐项对照或其邀请、讨论焦点和操作按钮。新提问与引导求助不再请求 `discussion-v1` 组件输出;“帮我看看”及生成的追问仍可使用,保留输入草稿。 +- 历史讨论数据及快照保留在 Main 的原存储中,旧回复正文、工具活动和解析失败时保存的原文仍可查看。升级前结果不确定的请求重试保持原请求身份与内容,避免重复执行。 - 咨询正文支持 Markdown 标题、列表、表格、代码围栏、HTTP(S) 链接/图片和数学公式,长代码与表格在栏内横向滚动,不执行 HTML。云端正文维持 string 合同,未知对象不会猜测转成回答。工具活动只从云端主线程的类型化工具事件及 Main 本地读取过程获得,按本次问题/工具调用身份合并名称和状态,跨云端暂停、续接仍只计一次;暂停读取不算失败,状态以实际读取结果或问题终态为准,单独折叠显示;参数、结果原文和内部错误不混入回复,思考内容不作为正文。客户端仅保存自己实际收到的工具状态,不能补回旧历史或断线期间已经过期的事件。 - 云端咨询在 local_context 声明 read_protocol=2;需先部署配套 Yuxi API 和 worker,再升级客户端。云端持久累计读取字节及批次,每轮告知模型剩余额度;预算耗尽后消费最后一批结果,并以 tool_choice=none 要求根据现有证据形成答案和说明缺口。Main 限制实际返回量并拒绝第十三批读取,区分读取达到上限、上下文失效及格式无效。服务端对未声明协议的已安装旧客户端保留原工具参数与六批边界。 - 智能体人设、职责、提示词和 Skills 由发布配置决定。Main 不追加固定教学基线、不生成“小麦”、不清空所选智能体的 Skills,只添加真实工具能力、上下文边界和本轮界面协议。智能体名称叫“老师”“朋友”或“代码顾问”不改变调用路径或权限。 -- 每轮格式由 Main 的对应意图协议决定;`discussion.ts` 为支持组件的请求注入唯一 `{reply, quickReplies, tool}` 协议。运营教学补充不另写字段协议或要求始终纯文字。工具内讨论保留类型、稳定 ID、未修改内容和采纳状态;暂停/未进入时 `tool:null`,没有实质变化时也可保留原内容。结构图、流程和对照目前没有独立的采纳/来源字段,待定、建议与预测只能在展示文字中明确,不能据此推导已确认共识。 -- 当前输入是按预算截取的所选操作对话已完成分支文本、明确引用、咨询历史和适用的当前组件,并支持上述只读原文恢复;没有跨全部操作会话自动读取,也未向操作 Agent 注入咨询共识。“先这些”仍仅结束本轮整理,保留各条目状态,不确认全部候选、不自动制作。提示词不宣称这些系统能力已接通,也不声称看过未提供的画面或试玩过作品。Yuxi 话题经云端原生执行,旧版本地智能体由 Main 调用计费网关;均不创建本地 Pi 编程 worker。 +- 每轮格式由 Main 的对应意图协议决定;当前咨询界面的新问题使用普通回答,“帮我看看”使用既有问题建议协议。旧请求的讨论协议解析与数据读取仍保留,客户端不再提供组件交互入口。 +- 当前输入是按预算截取的所选操作对话已完成分支文本、明确引用和咨询历史,并支持上述只读原文恢复;没有跨全部操作会话自动读取,也未向操作 Agent 注入咨询共识。提示词不宣称这些系统能力已接通,也不声称看过未提供的画面或试玩过作品。Yuxi 话题经云端原生执行,旧版本地智能体由 Main 调用计费网关;均不创建本地 Pi 编程 worker。 - 智能体输入裁剪区分两种计量:本地模型以约每 2 个 UTF-8 字节估算 1 Token,并计入工具与读取预留;这不是精确分词或扣费依据,模型上限与实际 usage 仍由服务端拥有。云端沿用 query 字节上限,以完整 JSON 转义后的大小裁剪来源节选。智能体指令、当前问题和明确引用不会被静默裁掉;固定配置或当前整理本身超限时明确提示调整配置,避免让学生反复缩短短问题或新建话题。完整来源仍可由只读工具按需读取。 - `tests/fixtures/teacher-guidance-examples.json` 提供宠物、种植、射击、蛋糕的人工多轮输入输出样例,由真实解析器、上下文编译器与讨论状态机校验;样例不加载到运行时,也不代表真实模型教学效果已验收。云端定义由 Yuxi 维护并由运营显式同步下发;本地代码修改不会自动改写云端配置或学生已接受问题的固定发布版本。 - 下发智能体的图标随发布版本保存,顶栏、咨询面板、欢迎区和消息使用定义中的 `icon_url`;未设置或图片加载失败时使用内置头像。运营在 Yuxi 更换图标后需同步下发配置,联系人使用当前下发的名称和图标;每轮执行仍记录实际发布版本。相对图片地址由服务端使用 Yuxi 公网入口补全。 diff --git a/src/pages/Chat/TeacherChatPanel.tsx b/src/pages/Chat/TeacherChatPanel.tsx index c790ec4f..3540411a 100644 --- a/src/pages/Chat/TeacherChatPanel.tsx +++ b/src/pages/Chat/TeacherChatPanel.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { ArrowUp, ChevronDown, ChevronRight, Loader2 } from 'lucide-react'; import { useAuthStore } from '@/stores/auth'; import { teacherApi, teacherTopicsPath, teacherConversationsPath, legacyTopicBase, type TeacherCatalogState } from '@/lib/coding-teacher'; @@ -10,8 +10,6 @@ import type { TeacherTopic, TeacherTopicList, } from '../../../shared/coding-teacher'; -import { TeacherDiscussionPanel, DiscussionFocus } from './TeacherDiscussionPanel'; -import type { TeacherDiscussionAction } from '../../../shared/coding-teacher'; import { TeacherAvatar } from './TeacherAvatar'; import { TeacherReply } from './TeacherReply'; @@ -87,7 +85,7 @@ function ProjectTeacherChatPanel({ try { localStorage.setItem(draftKey, JSON.stringify({ text, references, pending, ...referenceScopeRef.current })); } catch { /* Draft remains in the open panel. */ } }, [draftKey]); const label = '智能体'; - const discussionEnabled = !draftRevision; + const helpEnabled = !draftRevision; const [historyOpen, setHistoryOpen] = useState(false); const [before, setBefore] = useState(cachedView?.before ?? null); const [loadingOlder, setLoadingOlder] = useState(false); @@ -95,7 +93,6 @@ function ProjectTeacherChatPanel({ const followBottom = useRef(cachedView?.follow ?? true); const restoreTop = useRef(cachedView?.top); const scrollPosition = useRef(cachedView?.top ?? 0); - const [focus, setFocus] = useState<{ topicId: string; id: string; label: string } | null>(null); const [localDefinition, setDefinition] = useState(null); const catalogStateRef = useRef(catalogState); catalogStateRef.current = catalogState; @@ -107,10 +104,7 @@ function ProjectTeacherChatPanel({ const inputRef = useRef(null); const focusedOnce = useRef(false); const [topics, setTopics] = useState({ items: [], lastSelectedTopicId: null }); - const [storedTopic, setTopic] = useState(cachedView?.topic ?? null); - const topic = useMemo(() => storedTopic && continuous ? { ...storedTopic, - discussion: storedTopic.conversation?.discussions[projectId] ?? storedTopic.discussion } - : storedTopic, [storedTopic, continuous, projectId]); + const [topic, setTopic] = useState(cachedView?.topic ?? null); const legacyFriend = topic?.role === 'friend'; const legacyView = Boolean(continuous && topic && !topic.conversation); const viewingLegacy = useRef(false); @@ -158,12 +152,6 @@ function ProjectTeacherChatPanel({ if (continuous && draftKey && !viewingLegacy.current) conversationViews.set(draftKey, { topic: topicRef.current, before: beforeRef.current, top: scrollPosition.current, follow: followBottom.current }); }, [continuous, draftKey]); - 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; useLayoutEffect(() => { const pane = scrollRef.current; if (!pane) return; @@ -396,7 +384,7 @@ function ProjectTeacherChatPanel({ if (version === generation.current) setBusy(false); } }; - const send = async (action?: Pick) => { + const send = async (action?: Pick) => { const question = action?.text ?? text; const questionReferences = action ? [] : references; if (!question.trim() || busy || !topicEnabled || topicRef.current?.requests.some((request) => ['preparing', 'running'].includes(request.status))) return; @@ -422,27 +410,21 @@ function ProjectTeacherChatPanel({ consume(current); setDefinition(current.definition); } - const presentation = discussionEnabled && (!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; + // Retry an uncertain saved request verbatim, including pre-upgrade presentation fields. const input = previous && previous.text === question && (!continuous || previous.projectId === projectId) && previous.sourceConversationId === (sourceId || undefined) && - 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, ...(continuous ? { projectId } : {}), - ...(presentation ? { presentation } : {}), ...(discussion ? { discussion } : {}), ...(action?.intent ? { intent: action.intent } : {}), ...(sourceId ? { sourceConversationId: sourceId } : {}), }; @@ -472,34 +454,6 @@ function ProjectTeacherChatPanel({ (request) => request.status === 'preparing' || request.status === 'running' ); const helpUnavailable = busy || !topicEnabled || 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(topicBase, endpointId, { - ...(continuous ? { projectId } : {}), - 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); }; @@ -529,13 +483,6 @@ function ProjectTeacherChatPanel({ className="shrink-0 rounded-lg px-2 text-xs text-muted-foreground hover:bg-white hover:text-foreground disabled:opacity-40">新话题 } } - {discussionEnabled && topic?.discussion && 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 } }); - }} />}
{ const pane = scrollRef.current; if (!pane) return; scrollPosition.current = pane.scrollTop; @@ -576,8 +523,8 @@ function ProjectTeacherChatPanel({ } {['failed', 'cancelled', 'interrupted'].includes(request.status) && } - : request.response ? :
{['running', 'preparing'].includes(request.status) ? {request.discussionContext ? '正在梳理你的新想法…' : `${label}正在想…`} : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : ''}
} - {discussionEnabled && request.intent !== 'suggestions' && request.status === 'completed' && topic.discussion?.status !== 'active' && topic.discussion?.status !== 'offered' && Boolean(request.suggestedQuestions?.length) &&
+ : request.response ? :
{['running', 'preparing'].includes(request.status) ? {`${label}正在想…`} : ['cancelled', 'interrupted'].includes(request.status) ? '这次先停在这里。想继续时可以再说说。' : ''}
} + {helpEnabled && request.intent !== 'suggestions' && request.status === 'completed' && Boolean(request.suggestedQuestions?.length) &&
{request.suggestedQuestions?.map(question => )}
} {request.discussionError &&

{request.discussionError}

} @@ -597,7 +544,7 @@ function ProjectTeacherChatPanel({ {error &&

{error}

} {topic?.unsaved &&
回复尚未保存,请复制或重试保存。
} {!topicEnabled && !error && !busy &&

{legacyFriend ? '这是旧版内置朋友的话题,仅供查看。请选择智能体新建话题。' : `${label}暂未开放,历史仍可查看。`}

} -
{ e.preventDefault(); void send(); }}> + { e.preventDefault(); void send(); }}> {Boolean(unassignedDraft.text?.trim() || unassignedDraft.references?.length) &&
查看之前的项目草稿
@@ -614,12 +561,11 @@ function ProjectTeacherChatPanel({
} {references.length > 0 &&

{references.map((ref) => ref.text).join('\n')}

}
- {activeFocus && setFocus(null)} />}