fix: keep the welcome composer at the bottom
This commit is contained in:
@@ -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/.
|
||||
|
||||
@@ -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 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ export function CodingChatBody({ welcome, hidden, bubble, children }: {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useLayoutEffect(() => {
|
||||
const body = ref.current;
|
||||
if (!body || !welcome || hidden || !bubble) return;
|
||||
const content = body?.querySelector<HTMLElement>('.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 <div ref={ref} className="coding-chat-body" data-welcome={welcome} hidden={hidden}>{children}</div>;
|
||||
|
||||
@@ -835,7 +835,7 @@ export function CodingChatPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showWelcome ? <CodingStartHeading /> : targetConversationId
|
||||
{showWelcome ? <><CodingStartHeading /><CommunityInvitation /></> : targetConversationId
|
||||
? (
|
||||
<CodingConversationTimeline
|
||||
key={targetConversationId}
|
||||
@@ -965,7 +965,6 @@ export function CodingChatPanel({
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
{showWelcome && <CommunityInvitation />}
|
||||
</CodingChatBody>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -35,7 +35,7 @@ export function Layout() {
|
||||
<main className="classroom-main flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div className="classroom-tabs" role="tablist"><button role="tab" aria-selected="true">操作对话</button><button role="tab">作品</button></div>
|
||||
<CodingChatBody welcome={!sent} hidden={false} bubble={bubble}>
|
||||
<div className="classroom-chat-view min-h-0 flex-1">{!sent ? <CodingStartHeading /> : <p>第一条消息已发送(测试)</p>}</div>
|
||||
<div className="classroom-chat-view min-h-0 flex-1 flex-col">{!sent ? <><CodingStartHeading /><CommunityInvitation /></> : <p>第一条消息已发送(测试)</p>}</div>
|
||||
<div className="classroom-composer-view shrink-0">
|
||||
<CodingComposer value={draft} editable canSend={Boolean(draft)} preparing={false} recovering={false}
|
||||
runStatus="idle" mode="prompt" queue={{ items: [] }} error={null} recoverableError={false}
|
||||
@@ -43,7 +43,6 @@ export function Layout() {
|
||||
onModeChange={() => {}} onSubmit={() => { setSent(true); setDraft(''); }} onRecover={() => {}}
|
||||
onAddFiles={() => {}} onRemoveAttachment={() => {}} />
|
||||
</div>
|
||||
{!sent && <CommunityInvitation />}
|
||||
</CodingChatBody>
|
||||
</main>
|
||||
</div>;
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user