From e812371973fbfb5a03733c1307116cfef567d0a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=A8=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 29 Sep 2026 15:50:43 +0800 Subject: [PATCH] fix: keep the welcome composer at the bottom --- .../20260929-community-welcome-c724e5a1.md | 11 ++++++++++- README.md | 2 +- src/components/community/community.css | 14 +++++++------- src/pages/Chat/CodingChatBody.tsx | 11 ++++++----- src/pages/Chat/CodingChatPanel.tsx | 3 +-- tests/e2e/community-welcome.spec.ts | 17 +++++++++++++++++ tests/e2e/fixtures/community-welcome.tsx | 3 +-- tests/e2e/pi-coding-first-chat.spec.ts | 3 +++ 8 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md b/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md index 5b7bbf65..cba931d5 100644 --- a/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md +++ b/.project-docs/30-worklog/tasks/20260929-community-welcome-c724e5a1.md @@ -23,7 +23,7 @@ - Warm welcome heading, existing composer and a compact inspiration invitation share the empty-chat layout. Both the invitation and persistent sidebar button open only https://square.nianxx.cn/#explore in the system browser on explicit click. - No public project feed, preview cards, community store or iframe remains in this feature. Existing unrelated Main routes are unchanged. The composer remains mounted across the first send; teacher introductions keep their prior three-successful-operation-round behavior and receive responsive clearance. - Preserved the concurrent live help-label overlay (task 20260929-teacher-help-label-83a2f67c): the two buttons and suggestions text remain 帮我整体看🧠. Ported only its three source edits and synchronized existing expectations; peer checkout untouched. -- Temporary Renderer now loads this checkout's index-SmPpxLeP.js. Existing Main PID 62668 remains running; formal installation, cloud configuration, user history and remote branches unchanged. Kept this worktree for the live trial. +- Temporary Renderer now loads this checkout's index-DHC9QT47.js. Existing Main PID 62668 remains running; formal installation, cloud configuration, user history and remote branches unchanged. Kept this worktree for the live trial. ## Verification @@ -59,3 +59,12 @@ 3. Verify layout, click behavior, no community requests, drafts, repository checks and the actual temporary Renderer; preserve the concurrent label update and temporary profile. All three steps completed. Earlier public-feed investigation was feasibility work; no feed fetching or embedded browsing is present in the delivered feature. + +## Follow-up: Keep Composer At Bottom + +- User asks to move the chat input to the bottom. Continue this owned worktree; latest trial manifest points here and checkout is clean. Existing context/ownership gates remain valid. +- Move welcome and external square link into the scrolling content area, keep the same composer mounted at the bottom, and reserve teacher clearance only in the content area. Verify desktop/narrow/short windows, preserve drafts, then refresh the trial without activation. + +- Completed: composer is anchored below the independently scrolling welcome/link area, keeps its instance and draft, and remains centered while teacher clearance affects only the upper content. +- Verification: 33 focused unit tests, all 6 headless layout/interaction tests (including a 375×600 viewport), typecheck, scoped ESLint, build:vite, native E2E --list (20 cases) and diff checks passed. Visually reviewed the new desktop layout. +- Trial now shows the actual welcome screen with index-DHC9QT47.js, input bottom gap 16px, two teacher greetings and the square link; no cards, iframe or alert. Account, route and all 13 saved drafts preserved. Focus monitor recorded only Codex during the refresh. Evidence: trial composer-bottom-20260929/. diff --git a/README.md b/README.md index 8f3d1922..da3b72b7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Code 会话保存的模型被下架后,会提示重新选择可用模型;用 Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,源码提供四个模块入口;云智能体需配套配置 WS/Yuxi 服务。模块入口页按 Agents、Code、Canvas、Robot 排列,统一采用横向插画卡片;Agents 标语为“打造你想象中的AI助手”。工作区左上角入口点击后返回模块入口页: - `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。 - 会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话显示童趣欢迎语、输入框和「去作品广场逛逛」入口;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。 + 会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在上方显示童趣欢迎语和「去作品广场逛逛」入口,输入框固定在内容区域底部,上方内容可独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。 - `Makelore Agents|AI 智能体`:配置个人云智能体并试聊,发布后正式对话、接入个人微信或创建应用 API Key。支持流式回复、审批、排队请求、附件与产物、自动任务和活动历史;重新进入时恢复最近智能体与对话,未确认操作可按原输入重试。聊天可提出日程建议,由创建者核对能力、时间和费用后启用。知识库上传、替换及导入本人会话附件后自动解析并建立索引;文档卡片区分排队、解析、索引、可用和失败,持续刷新已加载文档,失败可原地重试;零文本内容不会显示为可用,新文档可用后才移除被替换的旧文档。支持删除知识内容;个人 MCP 凭据、Skills、子智能体可自助管理。支持智能体和会话归档、发布版本比较及恢复为草稿。除知识库 embedding 向量化外,模型调用(包括知识处理触发的对话模型调用)仍按原规则由创建者个人词元点数支付,可按智能体设置每次任务和每日上限,费用按时间、来源、应用分页统计;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限;应用调用者保有自己的内容空间。Main 管理云会话和本机文件,Renderer 通过 Host API 操作。配套服务接入见 [Yuxi MakeLore 说明](https://xerrors.github.io/Yuxi/advanced/makelore-agents.html)。 编辑采用全高桌面分栏,支持拖动调整与展开、独立滚动、小窗口配置/试用切换。首次创建按选用途、说要求、试一试引导,提供科学问答、故事、英语和笔记整理起点;回答方式可多选,特别要求直接编辑,仍以草稿的 system_prompt 保存。新助手预选云端目录的第一个可用模型并明确显示,可自行更换;已有助手不自动换模型。编辑区以“我的要求”和“高级设置”两个 Tab 切换,默认显示基础要求;高级设置包含能力、知识与限制,返回时保留上次分类,切换保留未保存的输入及当前试用。已开始使用的助手在顶部显示“修改已保存,尚未应用”或“已应用最新修改”,并提供显眼的“应用最新修改”按钮;保存只更新草稿,应用沿用同一发布操作,失败重试保留原操作身份。草稿明确保存(Ctrl/⌘+S),保存与切换页面保留当前试用及未发送文字;“保存并重新试用”使用最新保存的要求,保留上次试用供对比,并可复用上次问题,不自动发送。示例问题仅填入输入框;有运行、排队、审批或未确认操作时先处理当前试用。工具确认先展示实际操作说明与参数,完整技术详情可展开。确认“开始使用”会创建仅自己可用的发布版本并进入独立对话,微信渠道与应用接入仍单独配置。费用上限在限制中独立保存。Enter 发送、Shift+Enter 换行,F6 切换面板焦点、Esc 恢复分栏。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 diff --git a/src/components/community/community.css b/src/components/community/community.css index 0562e3df..1de04eeb 100644 --- a/src/components/community/community.css +++ b/src/components/community/community.css @@ -1,11 +1,11 @@ .coding-chat-body { display: flex; flex: 1; flex-direction: column; min-height: 0; min-width: 0; overflow: hidden; } .coding-chat-body[hidden] { display: none; } -.coding-chat-body[data-welcome="true"] { overflow-y: auto; padding: clamp(24px, 4vh, 48px) 24px 28px; background: radial-gradient(ellipse at 45% 18%, #fff8e8 0%, #fffdf8 28%, #fff 67%); } -.coding-chat-body[data-welcome="true"] .classroom-chat-view { flex: 0 0 auto; } -.coding-chat-body[data-welcome="true"] .classroom-composer-view { width: 100%; max-width: 740px; margin: 0 auto; } -.coding-chat-body[data-welcome="true"] [data-testid="coding-message-composer"] { width: 100%; padding: 0 0 28px; } +.coding-chat-body[data-welcome="true"] { padding: 0 24px 16px; background: radial-gradient(ellipse at 45% 18%, #fff8e8 0%, #fffdf8 28%, #fff 67%); } +.coding-chat-body[data-welcome="true"] .classroom-chat-view { flex: 1 1 0; overflow-y: auto; padding: clamp(24px, 4vh, 48px) 0 24px; } +.coding-chat-body[data-welcome="true"] .classroom-composer-view { width: 100%; max-width: 740px; margin: 0 auto; padding-top: 16px; } +.coding-chat-body[data-welcome="true"] [data-testid="coding-message-composer"] { width: 100%; padding: 0; } .coding-chat-body[data-welcome="true"] .chat-composer-surface { background: #fff; border-color: #d7dfda; border-radius: 20px; box-shadow: 0 5px 0 #eaf0e8, 0 12px 32px #69774b08; } -.coding-start-heading { position: relative; width: 100%; max-width: 740px; margin: 0 auto; padding: 0 12px 24px; text-align: center; } +.coding-start-heading { position: relative; flex-shrink: 0; width: 100%; max-width: 740px; margin: 0 auto; padding: 0 12px 24px; text-align: center; } .coding-start-mark { display: inline-grid; position: relative; place-items: center; width: 57px; height: 59px; border-radius: 18px; border: 1px solid #f2d6a7; background: #fff0d7; box-shadow: 0 4px 0 #eeddbd; transform: rotate(-7deg); animation: community-hello 650ms ease-out both; } .coding-start-mark img { width: 35px; height: 35px; } .coding-start-mark > svg { position: absolute; width: 17px; height: 17px; right: -24px; top: -2px; color: #c78b36; } @@ -14,7 +14,7 @@ .coding-start-heading h1 { color: #304968; font-size: clamp(24px, 2.5vw, 34px); line-height: 1.45; font-weight: 650; letter-spacing: -.04em; } .coding-start-heading h1 span { position: relative; white-space: nowrap; background: linear-gradient(transparent 73%, #ffe6a6 73%, #ffe6a6 94%, transparent 94%); } .coding-start-caption { margin-top: 10px; color: #718096; font-size: 13px; line-height: 1.8; } -.community-invitation { display: flex; align-items: center; justify-content: space-between; gap: 18px; width: 100%; max-width: 740px; margin: 5px auto 0; padding: 21px 23px; border: 1px dashed #e4d9bc; border-radius: 18px; background: #fffaf0; color: #304968; } +.community-invitation { display: flex; flex-shrink: 0; align-items: center; justify-content: space-between; gap: 18px; width: 100%; max-width: 740px; margin: 5px auto 0; padding: 21px 23px; border: 1px dashed #e4d9bc; border-radius: 18px; background: #fffaf0; color: #304968; } .community-invitation h2 { margin-bottom: 5px; font-size: 15px; font-weight: 600; } .community-invitation p { color: #7c806e; font-size: 12px; line-height: 1.7; } .community-link { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 40px; padding: 8px 13px; border: 1px solid #dce5d0; border-radius: 12px; background: #f0f5e8; color: #526b46; font-size: 13px; cursor: pointer; white-space: nowrap; } @@ -29,7 +29,7 @@ .community-link-error { display: block; margin-top: 6px; color: #965a45; font-size: 12px; line-height: 1.6; } @keyframes community-hello { 0% { transform: rotate(-15deg) translateY(5px); opacity: 0; } 70% { transform: rotate(3deg); opacity: 1; } 100% { transform: rotate(-7deg); } } @media (max-width: 600px) { - .coding-chat-body[data-welcome="true"] { padding: 24px 16px; } + .coding-chat-body[data-welcome="true"] { padding: 0 16px 16px; } .coding-start-caption { max-width: 230px; margin: 9px auto 0; } .community-invitation { flex-direction: column; align-items: flex-start; gap: 13px; padding: 19px; } } diff --git a/src/pages/Chat/CodingChatBody.tsx b/src/pages/Chat/CodingChatBody.tsx index a54f9588..26401ba3 100644 --- a/src/pages/Chat/CodingChatBody.tsx +++ b/src/pages/Chat/CodingChatBody.tsx @@ -7,14 +7,15 @@ export function CodingChatBody({ welcome, hidden, bubble, children }: { const ref = useRef(null); useLayoutEffect(() => { const body = ref.current; - if (!body || !welcome || hidden || !bubble) return; + const content = body?.querySelector('.classroom-chat-view'); + if (!body || !content || !welcome || hidden || !bubble) return; const update = () => { - const area = body.getBoundingClientRect(); + const area = content.getBoundingClientRect(); const message = bubble.getBoundingClientRect(); const overlaps = message.height > 0 && message.right > area.left && message.left < area.right && message.bottom > area.top; // Wide workspaces put the welcome beside the teachers. Compact ones flow below them. - body.style.paddingRight = overlaps && area.width >= 1050 ? `${Math.ceil(area.right - message.left + 24)}px` : ''; - body.style.paddingTop = overlaps && area.width < 1050 ? `${Math.ceil(message.bottom - area.top + 24)}px` : ''; + content.style.paddingRight = overlaps && area.width >= 1050 ? `${Math.ceil(area.right - message.left + 24)}px` : ''; + content.style.paddingTop = overlaps && area.width < 1050 ? `${Math.ceil(message.bottom - area.top + 24)}px` : ''; }; update(); const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update); @@ -22,7 +23,7 @@ export function CodingChatBody({ welcome, hidden, bubble, children }: { window.addEventListener('resize', update); return () => { observer?.disconnect(); window.removeEventListener('resize', update); - body.style.paddingRight = ''; body.style.paddingTop = ''; + content.style.paddingRight = ''; content.style.paddingTop = ''; }; }, [welcome, hidden, bubble]); return ; diff --git a/src/pages/Chat/CodingChatPanel.tsx b/src/pages/Chat/CodingChatPanel.tsx index eda736d0..2a1db052 100644 --- a/src/pages/Chat/CodingChatPanel.tsx +++ b/src/pages/Chat/CodingChatPanel.tsx @@ -835,7 +835,7 @@ export function CodingChatPanel({ )} - {showWelcome ? : targetConversationId + {showWelcome ? <> : targetConversationId ? ( } - {showWelcome && } diff --git a/tests/e2e/community-welcome.spec.ts b/tests/e2e/community-welcome.spec.ts index fe4d0ec7..37089807 100644 --- a/tests/e2e/community-welcome.spec.ts +++ b/tests/e2e/community-welcome.spec.ts @@ -65,10 +65,27 @@ for (const width of [375, 768, 1280, 1470]) { await expect(page.getByRole('button', { name: '去作品广场逛逛' })).toBeVisible(); await teachers.getByRole('button', { name: '先自己试试' }).click(); await heading.scrollIntoViewIfNeeded(); + const composer = page.getByTestId('coding-message-composer'); + await expect(composer).toBeInViewport({ ratio: 1 }); + const box = (await composer.boundingBox())!; + expect(850 - box.y - box.height).toBeLessThanOrEqual(40); + expect((await page.getByTestId('community-invitation').boundingBox())!.y).toBeLessThan(box.y); await page.screenshot({ path: test.info().outputPath(`community-welcome-${width}.png`) }); }); } +test('a short window scrolls the welcome while keeping the composer at the bottom', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 600 }); + await page.goto(baseUrl + '?teachers'); + const composer = page.getByTestId('coding-message-composer'); + await expect(composer).toBeInViewport({ ratio: 1 }); + const before = (await composer.boundingBox())!; + await page.getByTestId('community-invitation').scrollIntoViewIfNeeded(); + const after = (await composer.boundingBox())!; + expect(after.y).toBeCloseTo(before.y, 0); + expect(600 - after.y - after.height).toBeLessThanOrEqual(40); +}); + test('the community link opens only on click and leaves the draft mounted', async ({ page }) => { await page.setViewportSize({ width: 1280, height: 850 }); const communityRequests: string[] = []; diff --git a/tests/e2e/fixtures/community-welcome.tsx b/tests/e2e/fixtures/community-welcome.tsx index 5f18539e..e6df5fb2 100644 --- a/tests/e2e/fixtures/community-welcome.tsx +++ b/tests/e2e/fixtures/community-welcome.tsx @@ -35,7 +35,7 @@ export function Layout() {
; diff --git a/tests/e2e/pi-coding-first-chat.spec.ts b/tests/e2e/pi-coding-first-chat.spec.ts index fed1943a..162cb605 100644 --- a/tests/e2e/pi-coding-first-chat.spec.ts +++ b/tests/e2e/pi-coding-first-chat.spec.ts @@ -1110,6 +1110,9 @@ test('first PI Conversation is editable under 500 ms and submits before runtime await appendFile(performanceFragment, `${JSON.stringify({ editableMs })}\n`); } + await expect(page.getByTestId('coding-message-composer')).toBeInViewport({ ratio: 1 }); + expect(await page.getByTestId('coding-message-composer').evaluate(el => innerHeight - el.getBoundingClientRect().bottom)).toBeLessThan(40); + const pixelPng = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64',