diff --git a/docs/regression-guards/103-release-0630002-rollback.md b/docs/regression-guards/103-release-0630002-rollback.md index 17f8b75..00be58b 100644 --- a/docs/regression-guards/103-release-0630002-rollback.md +++ b/docs/regression-guards/103-release-0630002-rollback.md @@ -1,37 +1,42 @@ # 103 发布 0630002 — 回退说明 -**发布编号:** `20260630-013953-7906f3c` -**Git commit:** `7906f3c`(分支 `0630002`) +**发布编号:** `20260630-020301-14f46cf` +**Git commit:** `14f46cf`(分支 `0630002`) **发布时间:** 2026-06-30 ## 本次内容 -- **修复删除无效**:MySQL `source_snapshot_json` 经 mysql2 返回为 object 时,`JSON.parse(object)` 抛错导致 `relative_path` 为空,工作区 HTML 未清理,列表同步后页面「复活」 -- 新增 `parseJsonColumn()`,统一解析 JSON 列(`mindspace-pages.mjs`、`mindspace-page-sync.mjs`) -- 删除 purge 失败时增加 warn 日志 +- **待办提醒推送**:`schedule-reminder-worker` 扫描并投递单次 `h5_schedule_reminders`(微信服务号 + 站内通知) +- **行事历 UI**:仅展示「提醒」;状态「已通知 / 待通知」;支持忽略、勾选、全选、批量删除 +- **日程写入**:`startLocal` / `remindLocal` 强制本地时间解析,拒绝离谱 epoch 毫秒时间戳 +- **API**:`POST /mindspace/v1/schedule/reminders/:id/ignore`、`POST /mindspace/v1/schedule/reminders/bulk-delete` ## 103 自动备份(发布脚本已创建) | 类型 | 路径(103 上) | |------|----------------| -| 全量 live 备份 | `/Users/john/Project/backups/memind/memind-full-20260630-013953-7906f3c-before.tar.gz` | -| 持久目录备份 | `/Users/john/Project/backups/memind/memind-persisted-20260630-013953-7906f3c-before.tar.gz` | -| 旧源码归档 | `/Users/john/Project/archives/Memind-source-before-20260630-013953-7906f3c` | +| 全量 live 备份 | `/Users/john/Project/backups/memind/memind-full-20260630-020301-14f46cf-before.tar.gz` | +| 持久目录备份 | `/Users/john/Project/backups/memind/memind-persisted-20260630-020301-14f46cf-before.tar.gz` | +| 旧源码归档 | `/Users/john/Project/archives/Memind-source-before-20260630-020301-14f46cf` | | 当前 live | `/Users/john/Project/Memind`(无源码 runtime) | +**上一版 live(本发布前):** `20260630-013953-7906f3c`(commit `7906f3c`) + ## 健康验证 ```bash ssh 58.38.22.103 'curl -sf http://127.0.0.1:8081/api/status && echo' ``` -## 回退到上一版 runtime(0630001) +**2026-06-30 发版后验证:** `ok`;`.env` 中 `H5_SCHEDULE_ENABLED=1`、`H5_REMINDER_WORKER_ENABLED=1`。 + +## 回退到上一版 runtime(7906f3c) 在 **103** 上执行: ```bash -RELEASE_ID=20260630-013953-7906f3c -PREV_RELEASE=20260630-012244-66ca0b3 +RELEASE_ID=20260630-020301-14f46cf +PREV_RELEASE=20260630-013953-7906f3c APP_DIR=/Users/john/Project/Memind ARCHIVE=/Users/john/Project/archives/Memind-source-before-${RELEASE_ID} PORTAL_LABEL=cn.tkmind.memind-portal @@ -41,7 +46,6 @@ launchctl bootout "${LAUNCHD_GUI}/${PORTAL_LABEL}" 2>/dev/null || true lsof -tiTCP:8081 -sTCP:LISTEN | xargs kill -9 2>/dev/null || true mv "${APP_DIR}" "${APP_DIR}.failed-runtime-${RELEASE_ID}" -# 若需回到 0630001 runtime,从全量备份解压或保留的 runtime artifact 恢复 tar -xzf /Users/john/Project/backups/memind/memind-full-${PREV_RELEASE}-before.tar.gz -C /Users/john/Project launchctl bootstrap "${LAUNCHD_GUI}" "${HOME}/Library/LaunchAgents/${PORTAL_LABEL}.plist" @@ -51,13 +55,17 @@ curl -sf http://127.0.0.1:8081/api/status ## 从全量 tar 恢复(极端情况) +从**本发布前**全量备份恢复(即回到 `7906f3c` 上线前的 live 快照): + ```bash cd /Users/john/Project mv Memind "Memind.broken-$(date +%Y%m%d-%H%M%S)" -tar -xzf backups/memind/memind-full-20260630-013953-7906f3c-before.tar.gz +tar -xzf backups/memind/memind-full-20260630-020301-14f46cf-before.tar.gz # 再启动 Portal LaunchAgent ``` +若 runtime 切换失败,发布脚本 remote 段内置 `trap rollback` 会自动尝试恢复。 + ## 本地复现 / 再发版 ```bash diff --git a/mindspace-chat-context.mjs b/mindspace-chat-context.mjs index ab29ef3..8a1f13b 100644 --- a/mindspace-chat-context.mjs +++ b/mindspace-chat-context.mjs @@ -63,6 +63,75 @@ function resolvePageRecord(selectedPageId, pages, pageLive) { }; } +function normalizeSelectedAsset(asset) { + if (!asset?.id) return null; + return { + id: asset.id, + displayName: asset.displayName ?? asset.filename ?? asset.id, + mimeType: asset.mimeType ?? 'application/octet-stream', + ...(asset.filename ? { filename: asset.filename } : {}), + ...(typeof asset.sizeBytes === 'number' ? { sizeBytes: asset.sizeBytes } : {}), + ...(asset.categoryCode ? { categoryCode: asset.categoryCode } : {}), + ...(asset.assetType ? { assetType: asset.assetType } : {}), + }; +} + +function inferAssetKindLabel(asset) { + const mime = String(asset?.mimeType ?? '').toLowerCase(); + const name = String(asset?.displayName ?? asset?.filename ?? '').toLowerCase(); + if (mime.startsWith('image/')) return '图片'; + if ( + mime.includes('spreadsheet') || + mime.includes('csv') || + name.endsWith('.xlsx') || + name.endsWith('.xls') || + name.endsWith('.csv') + ) { + return '数据表'; + } + if (mime.includes('pdf') || name.endsWith('.pdf')) return '报告'; + if (mime.includes('word') || name.endsWith('.doc') || name.endsWith('.docx')) return '文档'; + if (mime.startsWith('text/')) return '文本资料'; + return '文件'; +} + +function buildSelectedAssetInstructions(context) { + const selected = context.selectedAssets ?? []; + if (selected.length === 0) return []; + + const lines = ['【界面勾选资料(硬性)】']; + if (selected.length === 1) { + const asset = selected[0]; + const kind = inferAssetKindLabel(asset); + lines.push( + `- 用户在界面勾选了 1 份${kind}:「${asset.displayName}」(id: ${asset.id},${asset.mimeType})`, + '- 用户说「这个 / 这份 / 这篇 / 这份报告 / 这个数据 / 帮我分析 / 帮我删除」等指代词时,**必须**指这份勾选资料,不得猜测其它文件。', + `- 读取/下载:GET /api/mindspace/v1/assets/${asset.id}/download`, + `- 删除:DELETE /api/mindspace/v1/assets/${asset.id}(执行删除前必须向用户确认)`, + ); + if (context.h5ApiBase) { + lines.push(`- H5 API 基址:${context.h5ApiBase}`); + } + } else { + lines.push(`- 用户在界面同时勾选了 ${selected.length} 份资料:`); + for (const asset of selected) { + lines.push(` · 「${asset.displayName}」(id: ${asset.id},${asset.mimeType})`); + } + lines.push( + '- 用户说「这个 / 这份」等指代词时,**必须先追问**要处理哪一份,不得擅自选一份执行。', + '- 不要主动对多选资料发起默认操作;等用户明确指定后再执行。', + ); + } + lines.push(''); + return lines; +} + +export function buildSelectedAssetGreeting(asset) { + const kind = inferAssetKindLabel(asset); + const name = asset.displayName ?? asset.filename ?? '这份资料'; + return `我看到你在界面里勾选了「${name}」。你是想查看或处理这份${kind}吗?可以直接说「帮我分析」「总结一下」或「帮我删除」——我会针对这份资料来操作。`; +} + export function buildMindSpaceChatContext(input) { const { space, @@ -72,6 +141,7 @@ export function buildMindSpaceChatContext(input) { pages = [], pageLive = null, assets = [], + selectedAssets = [], focusedAsset = null, route, agentSessionId = null, @@ -87,6 +157,14 @@ export function buildMindSpaceChatContext(input) { if (pageRecord && selectedPageId) view = 'page'; else if (selectedCategory) view = 'category'; + const normalizedSelected = selectedAssets + .map((asset) => normalizeSelectedAsset(asset)) + .filter(Boolean); + const resolvedFocus = + normalizedSelected.length === 1 + ? normalizedSelected[0] + : normalizeSelectedAsset(focusedAsset); + const context = { spaceId: space.id, spaceName: space.name, @@ -135,13 +213,14 @@ export function buildMindSpaceChatContext(input) { }, } : {}), - ...(focusedAsset + ...(resolvedFocus ? { - focusedAsset: { - id: focusedAsset.id, - displayName: focusedAsset.displayName, - mimeType: focusedAsset.mimeType, - }, + focusedAsset: resolvedFocus, + } + : {}), + ...(normalizedSelected.length + ? { + selectedAssets: normalizedSelected, } : {}), }; @@ -175,6 +254,14 @@ function buildLocationHint(context) { if (context.page) { return `用户正在查看/编辑页面「${context.page.title}」(id: ${context.page.id})。指代「这个页面」「改这里」时默认指该页面。`; } + if (context.selectedAssets?.length === 1) { + const asset = context.selectedAssets[0]; + const kind = inferAssetKindLabel(asset); + return `用户在界面勾选了${kind}「${asset.displayName}」(id: ${asset.id})。指代「这个文件」「这份报告」「这个数据」时**必须**指该勾选资料。`; + } + if (context.selectedAssets?.length > 1) { + return `用户在界面同时勾选了 ${context.selectedAssets.length} 份资料。指代「这个 / 这份」时必须先让用户说明要处理哪一份。`; + } if (context.focusedAsset) { return `用户正在处理资料「${context.focusedAsset.displayName}」(id: ${context.focusedAsset.id})。指代「这个文件」「这份资料」时默认指该资产。`; } @@ -274,6 +361,8 @@ export function buildContextPrefix(context) { ); } + lines.push(...buildSelectedAssetInstructions(context)); + if (context.homeCategories?.length) { lines.push('- 空间分类概览:'); for (const item of context.homeCategories) { @@ -304,6 +393,12 @@ export function formatContextChip(context) { typeof context.page.versionNo === 'number' ? ` · v${context.page.versionNo}` : ''; return `编辑 · ${context.page.title}${version}`; } + if (context.selectedAssets?.length === 1) { + return `已选 · ${context.selectedAssets[0].displayName}`; + } + if (context.selectedAssets?.length > 1) { + return `已选 ${context.selectedAssets.length} 项资料`; + } if (context.page) { const version = typeof context.page.versionNo === 'number' ? ` · v${context.page.versionNo}` : ''; @@ -320,4 +415,6 @@ export function formatContextChip(context) { export const mindspaceChatContextInternals = { stripHtml, excerptContent, + inferAssetKindLabel, + normalizeSelectedAsset, }; diff --git a/mindspace-chat-context.test.mjs b/mindspace-chat-context.test.mjs index 5017de6..1648f8d 100644 --- a/mindspace-chat-context.test.mjs +++ b/mindspace-chat-context.test.mjs @@ -3,6 +3,7 @@ import test from 'node:test'; import { buildContextPrefix, buildMindSpaceChatContext, + buildSelectedAssetGreeting, formatContextChip, mindspaceChatContextInternals, } from './mindspace-chat-context.mjs'; @@ -169,3 +170,103 @@ test('excerptContent strips html and truncates long text', () => { `${'a'.repeat(9)}…`, ); }); + +test('buildMindSpaceChatContext exposes single selected asset as focused asset', () => { + const asset = { + id: 'asset-9', + displayName: 'Q1-report.pdf', + mimeType: 'application/pdf', + filename: 'Q1-report.pdf', + sizeBytes: 1200, + categoryCode: 'oa', + assetType: 'file', + }; + const context = buildMindSpaceChatContext({ + space: sampleSpace, + selectedCategory: sampleCategory, + selectedPageId: null, + pages: [], + assets: [asset], + selectedAssets: [asset], + route: '/space?category=oa', + }); + + assert.equal(context.selectedAssets?.length, 1); + assert.equal(context.focusedAsset?.id, 'asset-9'); + assert.equal(formatContextChip(context), '已选 · Q1-report.pdf'); +}); + +test('buildContextPrefix binds deictic references to a single checked asset', () => { + const asset = { + id: 'asset-9', + displayName: 'sales.xlsx', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + filename: 'sales.xlsx', + categoryCode: 'oa', + }; + const prefix = buildContextPrefix( + buildMindSpaceChatContext({ + space: sampleSpace, + selectedCategory: sampleCategory, + selectedPageId: null, + pages: [], + assets: [asset], + selectedAssets: [asset], + route: '/space?category=oa', + h5ApiBase: 'http://127.0.0.1:8081', + }), + ); + + assert.match(prefix, /【界面勾选资料(硬性)】/); + assert.match(prefix, /sales\.xlsx(id: asset-9/); + assert.match(prefix, /DELETE \/api\/mindspace\/v1\/assets\/asset-9/); + assert.match(prefix, /指代「这个文件」「这份报告」「这个数据」时\*\*必须\*\*指该勾选资料/); +}); + +test('buildContextPrefix requires clarification when multiple assets are checked', () => { + const assets = [ + { id: 'asset-a', displayName: 'a.pdf', mimeType: 'application/pdf' }, + { id: 'asset-b', displayName: 'b.csv', mimeType: 'text/csv' }, + ]; + const prefix = buildContextPrefix( + buildMindSpaceChatContext({ + space: sampleSpace, + selectedCategory: sampleCategory, + selectedPageId: null, + pages: [], + assets, + selectedAssets: assets, + route: '/space?category=oa', + }), + ); + + assert.match(prefix, /同时勾选了 2 份资料/); + assert.match(prefix, /必须先追问/); + assert.doesNotMatch(prefix, /当前聚焦资料/); + assert.equal(formatContextChip(buildMindSpaceChatContext({ + space: sampleSpace, + selectedCategory: sampleCategory, + selectedPageId: null, + pages: [], + assets, + selectedAssets: assets, + route: '/space?category=oa', + })), '已选 2 项资料'); +}); + +test('buildSelectedAssetGreeting adapts wording to asset kind', () => { + assert.match( + buildSelectedAssetGreeting({ + displayName: 'sales.xlsx', + mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }), + /这份数据表/, + ); + assert.match( + buildSelectedAssetGreeting({ + displayName: 'review.pdf', + mimeType: 'application/pdf', + }), + /这份报告/, + ); +}); diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 8c8ffe8..1ca3883 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -297,7 +297,7 @@ export function ChatPanel({ : chatState === 'waiting' ? '消息已收到,正在连接后台…' : compact - ? '在这里继续和 Agent 对话…' + ? '随时召唤你的小助手吧' : randomPrompt; const mergeVoiceText = (spoken: string) => { diff --git a/src/components/MindSpaceSpaceChat.tsx b/src/components/MindSpaceSpaceChat.tsx index 5a9a9a2..f494f5c 100644 --- a/src/components/MindSpaceSpaceChat.tsx +++ b/src/components/MindSpaceSpaceChat.tsx @@ -1,9 +1,29 @@ -import { useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useChat } from '../context/ChatProvider'; -import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types'; +import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext'; +import type { Message, MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types'; import { SpaceChatFab } from './SpaceChatFab'; import { SpaceChatPanel } from './SpaceChatPanel'; +const SELECTED_ASSET_GREETING_ID = 'mindspace-selected-asset-greeting'; + +function buildSelectedAssetGreetingMessage( + asset: NonNullable[number], +): Message { + const text = buildSelectedAssetGreeting(asset); + return { + id: SELECTED_ASSET_GREETING_ID, + role: 'assistant', + created: Math.floor(Date.now() / 1000), + content: [{ type: 'text', text }], + metadata: { + userVisible: true, + agentVisible: false, + displayText: text, + }, + }; +} + export function MindSpaceSpaceChat({ context, user, @@ -31,15 +51,64 @@ export function MindSpaceSpaceChat({ hideOpenFullChat?: boolean; title?: string; }) { - const { chatState, pendingTool } = useChat(); + const { chatState, pendingTool, newSession, switchSession, session, messages } = useChat(); const [internalOpen, setInternalOpen] = useState(false); const open = controlledOpen ?? internalOpen; const setOpen = onOpenChange ?? setInternalOpen; + const wasOpenRef = useRef(false); + const restoreSessionIdRef = useRef(null); + const messagesRef = useRef(messages); + messagesRef.current = messages; + + useEffect(() => { + if (chatBridge) { + wasOpenRef.current = open; + return; + } + + const wasOpen = wasOpenRef.current; + if (open && !wasOpen) { + restoreSessionIdRef.current = session?.id ?? null; + newSession(); + } else if (!open && wasOpen) { + if (messagesRef.current.length === 0 && restoreSessionIdRef.current) { + switchSession(restoreSessionIdRef.current); + } + restoreSessionIdRef.current = null; + } + wasOpenRef.current = open; + }, [chatBridge, newSession, open, session?.id, switchSession]); + + useEffect(() => { + if (chatBridge) return; + return () => { + if (messagesRef.current.length === 0 && restoreSessionIdRef.current) { + switchSession(restoreSessionIdRef.current); + } + }; + }, [chatBridge, switchSession]); + + const prefillMessages = useMemo(() => { + if (chatBridge || !open) return []; + if (messages.some((message) => message.role === 'user')) return []; + const selected = context.selectedAssets ?? []; + if (selected.length !== 1) return []; + return [buildSelectedAssetGreetingMessage(selected[0])]; + }, [chatBridge, context.selectedAssets, messages, open]); + const wrapperClass = variant === 'overlay' ? 'space-chat-stack space-chat-stack-overlay' : 'space-chat-stack'; return (
+ {open ? ( +
); diff --git a/src/components/MindSpaceView.tsx b/src/components/MindSpaceView.tsx index 24dbab9..e2aa38d 100644 --- a/src/components/MindSpaceView.tsx +++ b/src/components/MindSpaceView.tsx @@ -667,6 +667,14 @@ export function MindSpaceView({ return () => document.removeEventListener('pointerdown', handlePointerDown); }, [openAssetMenuId]); + const selectedAssetsForChat = useMemo( + () => + selectedAssetIds + .map((id) => assets.find((asset) => asset.id === id)) + .filter((asset): asset is MindSpaceAsset => Boolean(asset)), + [selectedAssetIds, assets], + ); + const mindspaceChatContext = useMemo(() => { if (!space) return null; return buildMindSpaceChatContext({ @@ -677,7 +685,7 @@ export function MindSpaceView({ pages, pageLive: pageLiveContext, assets, - focusedAsset: agentAsset, + selectedAssets: selectedAssetsForChat, route: `${location.pathname}${location.search}`, agentSessionId: session?.id ?? null, h5ApiBase, @@ -690,7 +698,7 @@ export function MindSpaceView({ pages, pageLiveContext, assets, - agentAsset, + selectedAssetsForChat, location.pathname, location.search, session?.id, diff --git a/src/components/SpaceChatPanel.tsx b/src/components/SpaceChatPanel.tsx index c8c4e72..052a9fa 100644 --- a/src/components/SpaceChatPanel.tsx +++ b/src/components/SpaceChatPanel.tsx @@ -1,8 +1,8 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useChat } from '../context/ChatProvider'; import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat'; import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat'; -import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types'; +import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types'; import { formatContextChip } from '../utils/mindspaceChatContext'; import { ChatPanel } from './ChatPanel'; import { TKMindAvatar } from './TKMindAvatar'; @@ -17,6 +17,7 @@ export function SpaceChatPanel({ chatBridge, hideOpenFullChat = false, title = 'TKMind', + prefillMessages = [], }: { open: boolean; context: MindSpaceChatContext; @@ -31,6 +32,7 @@ export function SpaceChatPanel({ chatBridge?: PageEditSubChatBridge; hideOpenFullChat?: boolean; title?: string; + prefillMessages?: Message[]; }) { const mainChat = useChat(); const chat = chatBridge ?? mainChat; @@ -59,6 +61,15 @@ export function SpaceChatPanel({ return () => window.removeEventListener('keydown', handleKeyDown); }, [open, onClose]); + const displayMessages = useMemo(() => { + if (!prefillMessages.length) return messages; + if (messages.some((message) => message.role === 'user')) return messages; + const extras = prefillMessages.filter( + (item) => !messages.some((message) => message.id && message.id === item.id), + ); + return extras.length ? [...messages, ...extras] : messages; + }, [messages, prefillMessages]); + if (!open) return null; const contextLabel = formatContextChip(context); @@ -119,7 +130,7 @@ export function SpaceChatPanel({ ); diff --git a/src/index.css b/src/index.css index 5c1d7d3..84dfaa7 100644 --- a/src/index.css +++ b/src/index.css @@ -8769,11 +8769,12 @@ body, padding: 0; border: 0; border-radius: 999px; - color: inherit; - background: #fffaf0; + color: #18211d; + background: + linear-gradient(145deg, rgba(255, 252, 244, 0.98), rgba(229, 237, 226, 0.96)); box-shadow: - 0 10px 30px rgba(24, 33, 29, 0.18), - 0 0 0 1px rgba(47, 111, 87, 0.18); + 0 10px 30px rgba(24, 33, 29, 0.16), + 0 0 0 1px rgba(47, 111, 87, 0.16); cursor: pointer; transition: transform 0.18s ease, box-shadow 0.18s ease; } @@ -8782,6 +8783,16 @@ body, transform: translateY(-2px); } +.space-chat-backdrop { + position: fixed; + inset: 0; + z-index: 899; + border: 0; + padding: 0; + background: transparent; + cursor: default; +} + .space-chat-fab.is-streaming { box-shadow: 0 10px 30px rgba(47, 111, 87, 0.28), @@ -8815,15 +8826,59 @@ body, right: 24px; bottom: 92px; z-index: 900; - display: grid; - grid-template-rows: auto auto minmax(0, 1fr) auto; + display: flex; + flex-direction: column; width: min(480px, calc(100vw - 32px)); height: min(620px, calc(100vh - 120px)); + max-height: min(620px, calc(100vh - 120px)); overflow: hidden; - border: 1px solid rgba(24, 33, 29, 0.12); + border: 1px solid rgba(24, 33, 29, 0.14); border-radius: 24px; - background: rgba(255, 252, 244, 0.98); - box-shadow: 0 24px 60px rgba(24, 33, 29, 0.18); + color: #18211d; + background: + linear-gradient(160deg, rgba(255, 252, 244, 0.98) 0%, rgba(244, 240, 231, 0.98) 52%, rgba(229, 237, 226, 0.94) 100%); + box-shadow: + 0 24px 60px rgba(24, 33, 29, 0.16), + 0 0 0 1px rgba(47, 111, 87, 0.08); + + --ms-ink: #18211d; + --ms-muted: #6d7771; + --ms-soft: #52605a; + --ms-green: #2f6f57; + --ms-green-deep: #245742; + --ms-cream: #fffaf0; + --ms-paper: rgba(255, 252, 244, 0.96); + --ms-line: rgba(24, 33, 29, 0.12); + --ms-line-soft: rgba(24, 33, 29, 0.08); + + --color-bg-base: #f4f0e7; + --color-bg-surface: rgba(255, 255, 255, 0.82); + --color-bg-elevated: var(--ms-paper); + --color-bg-active: rgba(47, 111, 87, 0.12); + --color-bg-code: rgba(24, 33, 29, 0.06); + --color-bg-warning: rgba(238, 176, 78, 0.14); + --color-bg-error: rgba(214, 69, 69, 0.1); + --color-bg-info: rgba(47, 111, 87, 0.1); + --color-bg-hover: rgba(24, 33, 29, 0.06); + + --color-text-primary: var(--ms-ink); + --color-text-secondary: var(--ms-soft); + --color-text-muted: var(--ms-muted); + --color-text-faint: #8a958f; + --color-text-meta: #8a958f; + --color-text-link: var(--ms-green); + --color-text-error: #8a1f1f; + --color-text-info: var(--ms-green-deep); + + --color-border: var(--ms-line); + --color-border-input: rgba(24, 33, 29, 0.16); + --color-border-strong: rgba(47, 111, 87, 0.28); + --color-border-warning: rgba(238, 176, 78, 0.35); + --color-border-input-focus: rgba(47, 111, 87, 0.42); + + --color-accent: var(--ms-green); + --color-accent-user: var(--ms-green-deep); + --color-danger: #c4453d; } .space-chat-panel-header { @@ -8831,8 +8886,11 @@ body, align-items: center; justify-content: space-between; gap: 12px; + flex-shrink: 0; padding: 14px 16px; - border-bottom: 1px solid rgba(24, 33, 29, 0.08); + border-bottom: 1px solid var(--ms-line-soft); + background: rgba(255, 252, 244, 0.72); + backdrop-filter: blur(12px); } .space-chat-panel-brand { @@ -8845,12 +8903,13 @@ body, .space-chat-panel-brand strong { display: block; font-size: 14px; + color: var(--ms-ink); } .space-chat-context-chip { display: block; overflow: hidden; - color: #6d7771; + color: var(--ms-muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; @@ -8868,11 +8927,29 @@ body, height: 32px; border: 0; border-radius: 999px; - color: #52605a; + color: var(--ms-soft); font-size: 22px; line-height: 1; background: rgba(24, 33, 29, 0.06); cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} + +.space-chat-panel-close:hover { + color: var(--ms-green-deep); + background: rgba(47, 111, 87, 0.1); +} + +.space-chat-panel-actions .ghost-btn { + background: rgba(255, 255, 255, 0.72); + color: var(--ms-soft); + border: 1px solid rgba(24, 33, 29, 0.12); +} + +.space-chat-panel-actions .ghost-btn:hover:not(:disabled) { + color: var(--ms-green-deep); + border-color: rgba(47, 111, 87, 0.24); + background: rgba(47, 111, 87, 0.08); } .space-chat-panel-error { @@ -8880,6 +8957,7 @@ body, align-items: center; justify-content: space-between; gap: 12px; + flex-shrink: 0; padding: 10px 16px; color: #8a1f1f; font-size: 13px; @@ -8887,8 +8965,11 @@ body, } .space-chat-panel-body { + flex: 1; + min-height: 0; overflow: auto; padding: 12px 14px 0; + background: transparent; } .space-chat-panel-body .msg-content { @@ -8910,27 +8991,141 @@ body, .space-chat-panel-body .empty-state-compact { padding: 28px 12px; + color: var(--ms-muted); } .space-chat-panel-body .empty-state-compact h2 { font-size: 20px; + color: var(--ms-ink); } .space-chat-panel-body .empty-state-compact p { font-size: 13px; + color: var(--ms-muted); +} + +.space-chat-panel .msg-bubble-assistant { + background: rgba(255, 255, 255, 0.92); + color: var(--ms-ink); + border-color: rgba(24, 33, 29, 0.1); + box-shadow: 0 2px 8px rgba(24, 33, 29, 0.04); +} + +.space-chat-panel .msg-bubble-assistant::before { + border-right-color: rgba(255, 255, 255, 0.92); +} + +.space-chat-panel .msg-bubble-user { + background: var(--ms-green); + color: var(--ms-cream); + box-shadow: 0 2px 10px rgba(47, 111, 87, 0.22); +} + +.space-chat-panel .msg-bubble-user::before { + border-left-color: var(--ms-green); +} + +.space-chat-panel .thinking { + color: var(--ms-muted); } .space-chat-panel-footer { display: grid; gap: 10px; + flex-shrink: 0; padding: 12px 16px 16px; - border-top: 1px solid rgba(24, 33, 29, 0.08); - background: rgba(255, 252, 244, 0.96); + border-top: 1px solid var(--ms-line-soft); + background: rgba(255, 252, 244, 0.82); + backdrop-filter: blur(12px); +} + +.space-chat-panel-footer .chat-input-shell { + border: 1px solid rgba(24, 33, 29, 0.14); + border-radius: 16px; + background: rgba(255, 255, 255, 0.88); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.space-chat-panel-footer .chat-input-shell:focus-within { + border-color: rgba(47, 111, 87, 0.35); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.65), + 0 0 0 3px rgba(47, 111, 87, 0.1); +} + +.space-chat-panel-footer textarea.input.chat-input-field { + color: var(--ms-ink); + background: transparent; + border-color: transparent; + font-size: 13px; +} + +.space-chat-panel-footer textarea.input.chat-input-field::placeholder { + color: rgba(109, 119, 113, 0.85); + font-size: 13px; +} + +.space-chat-panel-footer textarea.input.chat-input-field:focus { + outline: none; + border-color: transparent; +} + +.space-chat-panel-footer .chat-image-upload-trigger { + color: var(--ms-soft); +} + +.space-chat-panel-footer .chat-image-upload-trigger:hover:not(:disabled) { + color: var(--ms-green-deep); + background: rgba(47, 111, 87, 0.1); +} + +.space-chat-panel-footer .voice-btn { + color: var(--ms-soft); + border-color: rgba(24, 33, 29, 0.14); + background: rgba(255, 255, 255, 0.72); +} + +.space-chat-panel-footer .voice-btn:hover:not(:disabled) { + color: var(--ms-green-deep); + border-color: rgba(47, 111, 87, 0.24); + background: rgba(47, 111, 87, 0.08); +} + +.space-chat-panel-footer .chat-skill-trigger { + border-color: rgba(24, 33, 29, 0.14); + background: rgba(255, 255, 255, 0.72); + color: var(--ms-soft); +} + +.space-chat-panel-footer .chat-skill-trigger:hover:not(:disabled) { + color: var(--ms-green-deep); + border-color: rgba(47, 111, 87, 0.24); + background: rgba(47, 111, 87, 0.08); +} + +.space-chat-panel-footer .send-btn { + min-width: 72px; + border-radius: 14px; + color: var(--ms-cream); + background: var(--ms-green); + box-shadow: 0 8px 18px rgba(47, 111, 87, 0.22); +} + +.space-chat-panel-footer .send-btn:hover:not(:disabled) { + background: var(--ms-green-deep); +} + +.space-chat-panel-footer .danger-btn { + border-radius: 14px; } .space-chat-panel-footer .chat-input-row .ghost-btn { padding: 8px 10px; font-size: 13px; + color: var(--ms-soft); + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(24, 33, 29, 0.12); } .space-chat-panel-dismiss { @@ -8938,7 +9133,7 @@ body, padding: 8px 12px; border: 0; border-radius: 12px; - color: #6d7771; + color: var(--ms-muted); font-size: 13px; background: rgba(24, 33, 29, 0.05); cursor: pointer; @@ -8946,13 +9141,66 @@ body, } .space-chat-panel-dismiss:hover { - color: #52605a; + color: var(--ms-soft); background: rgba(24, 33, 29, 0.09); } -.tool-confirm-compact { - margin: 0 16px 8px; +.space-chat-panel .tool-confirm-compact { + margin: 0 14px 8px; + flex-shrink: 0; border-radius: 16px; + color: var(--ms-ink); + border-color: rgba(238, 176, 78, 0.35); + background: rgba(238, 176, 78, 0.12); +} + +.space-chat-panel .tool-confirm-compact .tool-confirm-text { + color: var(--ms-ink); +} + +.space-chat-panel .tool-confirm-compact .tool-confirm-actions button { + background: var(--ms-green); + color: var(--ms-cream); +} + +.space-chat-panel .tool-confirm-compact .tool-confirm-actions .danger { + background: var(--color-danger); +} + +.space-chat-panel .msg-copy { + background: rgba(24, 33, 29, 0.06); + color: var(--ms-muted); +} + +.space-chat-panel .msg-copy:hover { + background: rgba(47, 111, 87, 0.12); + color: var(--ms-green-deep); +} + +.space-chat-panel .msg-save-page, +.space-chat-panel .msg-public-share-link { + background: rgba(255, 255, 255, 0.88); + border-color: rgba(24, 33, 29, 0.12); + color: var(--ms-soft); +} + +.space-chat-panel .chat-skill-menu { + border-color: rgba(24, 33, 29, 0.12); + background: rgba(255, 252, 244, 0.98); + box-shadow: 0 12px 32px rgba(24, 33, 29, 0.14); +} + +.space-chat-panel .chat-skill-menu-item { + color: var(--ms-ink); +} + +.space-chat-panel .chat-skill-menu-item:hover { + background: rgba(47, 111, 87, 0.08); +} + +.space-chat-panel .chat-history-loader { + border-color: rgba(24, 33, 29, 0.1); + color: var(--ms-muted); } @media (max-width: 720px) { @@ -8966,6 +9214,7 @@ body, bottom: 0; width: 100vw; height: min(75vh, calc(100vh - 48px)); + max-height: min(75vh, calc(100vh - 48px)); border-right: 0; border-bottom: 0; border-radius: 24px 24px 0 0; @@ -9065,6 +9314,10 @@ body, z-index: 1010; } +.space-chat-stack-overlay .space-chat-backdrop { + z-index: 1009; +} + @media (max-width: 720px) { .mindspace-page-fullscreen-preview-toolbar { align-items: flex-start; diff --git a/src/types.ts b/src/types.ts index 6a2fa85..fb5e36f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,7 +71,20 @@ export type MindSpaceChatContext = { id: string; displayName: string; mimeType: string; + filename?: string; + sizeBytes?: number; + categoryCode?: MindSpaceCategory['code']; + assetType?: string; }; + selectedAssets?: Array<{ + id: string; + displayName: string; + mimeType: string; + filename?: string; + sizeBytes?: number; + categoryCode?: MindSpaceCategory['code']; + assetType?: string; + }>; homeCategories?: Array<{ code: MindSpaceCategory['code']; name: string; diff --git a/src/utils/mindspaceChatContext.ts b/src/utils/mindspaceChatContext.ts index 2459b1b..679b154 100644 --- a/src/utils/mindspaceChatContext.ts +++ b/src/utils/mindspaceChatContext.ts @@ -11,6 +11,7 @@ export type { MindSpaceChatContext } from '../types'; export { buildContextPrefix, buildMindSpaceChatContext, + buildSelectedAssetGreeting, formatContextChip, } from '../../mindspace-chat-context.mjs'; @@ -27,6 +28,7 @@ export type BuildMindSpaceChatContextInput = { content: string; } | null; assets?: MindSpaceAsset[]; + selectedAssets?: MindSpaceAsset[]; focusedAsset?: MindSpaceAsset | null; route: string; agentSessionId?: string | null;