diff --git a/apps/desktop/src/main/gateway/document-gateway-bridge.ts b/apps/desktop/src/main/gateway/document-gateway-bridge.ts index 157ead19..312554bc 100644 --- a/apps/desktop/src/main/gateway/document-gateway-bridge.ts +++ b/apps/desktop/src/main/gateway/document-gateway-bridge.ts @@ -18,6 +18,8 @@ import type { CompleteExternalDocumentPatchInput, ExternalDocumentProjectionBinding, ImportRoomDocumentInput, + MoveRoomDocumentsResult, + MovedRoomDocument, PreparedExternalDocumentPatch, ExternalDocumentSourceKind, PrepareExternalDocumentPatchInput, @@ -297,6 +299,20 @@ export class DocumentGatewayBridge { }, true) } + move(documentId: string, fromRoomId: string, roomId: string): Promise { + return this.request(`/v1/documents/${encodeURIComponent(documentId)}/move`, { + method: 'POST', + body: JSON.stringify({ fromRoomId, roomId }), + }, true) + } + + moveMany(documentIds: string[], fromRoomId: string, roomId: string): Promise { + return this.request('/v1/documents/move', { + method: 'POST', + body: JSON.stringify({ documentIds, fromRoomId, roomId }), + }, true) + } + delete(documentId: string): Promise { return this.request(`/v1/documents/${encodeURIComponent(documentId)}`, { method: 'DELETE' }) } diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index a63e38ac..3cd6983b 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -452,6 +452,8 @@ const DOCUMENT_CHANNELS = { import: 'documents:import', save: 'documents:save', delete: 'documents:delete', + move: 'documents:move', + moveMany: 'documents:move-many', restore: 'documents:restore', deletePermanently: 'documents:delete-permanently', emptyTrash: 'documents:empty-trash', @@ -2713,6 +2715,8 @@ function registerDocumentHandlers( if (vaultSyncDeps) await trashDocumentInVault(vaultSyncDeps, documentId) return deleted }, + move: (_event, documentId, fromRoomId, roomId) => bridge.move(documentId, fromRoomId, roomId), + moveMany: (_event, documentIds, fromRoomId, roomId) => bridge.moveMany(documentIds, fromRoomId, roomId), restore: (_event, documentId) => bridge.restore(documentId), deletePermanently: async (_event, documentId) => { if (vaultSyncDeps) await trashDocumentInVault(vaultSyncDeps, documentId) diff --git a/apps/desktop/src/main/office/office-preview-registry.ts b/apps/desktop/src/main/office/office-preview-registry.ts index b8290a87..2c84a73c 100644 --- a/apps/desktop/src/main/office/office-preview-registry.ts +++ b/apps/desktop/src/main/office/office-preview-registry.ts @@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron' import { webContents } from 'electron' import type { OfficeAgentFileEvent } from '../../shared/office' +import { logDesktop } from '../logging/desktop-logger' import { officePreviewKindForFileName, type OfficePreviewKind, @@ -160,6 +161,16 @@ export class OfficePreviewRegistry { private wireDocsThesisHook(runtime: PreparedGenOfficeRuntime): void { const bridge = this.thesisBridge if (!bridge) return + // 运行时能力守卫:论文排版桥是 genoffice 后加的导出,带着旧运行时启动时它是 undefined。 + // 缺了只跳过这个桥(AI 页签排版面板不可用),不能让 ensureRuntime 抛错——那会让 + // 整条 open() 失败,Room 里的 Office 产物全部退回「用系统默认应用打开」。 + if (typeof runtime.docs.setDocsThesisPanelHook !== 'function') { + logDesktop('office', 'warn', { + event: 'office.thesis-bridge-unavailable', + message: 'genoffice runtime 缺少 setDocsThesisPanelHook,跳过论文排版桥', + }) + return + } runtime.docs.setDocsThesisPanelHook(async (wcId, action) => { const instance = this.findByWebContentsId(wcId) const roomId = instance?.roomId ?? null diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 0f827e5f..e28b41e1 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -821,6 +821,8 @@ const api: NxcoreDesktopApi = { import: (input) => invoke('documents:import', input), save: (documentId, input) => invoke('documents:save', documentId, input), delete: (documentId) => invoke('documents:delete', documentId), + move: (documentId, fromRoomId, roomId) => invoke('documents:move', documentId, fromRoomId, roomId), + moveMany: (documentIds, fromRoomId, roomId) => invoke('documents:move-many', documentIds, fromRoomId, roomId), restore: (documentId) => invoke('documents:restore', documentId), deletePermanently: (documentId) => invoke('documents:delete-permanently', documentId), emptyTrash: (roomId) => invoke('documents:empty-trash', roomId), diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/ContextRoom.css b/apps/desktop/src/renderer/src/components/context-room/ported/ContextRoom.css index 765cc80a..96084009 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/ContextRoom.css +++ b/apps/desktop/src/renderer/src/components/context-room/ported/ContextRoom.css @@ -3690,15 +3690,18 @@ --cr-border: #e5e7eb; --cr-blue: #408cf0; --cr-blue-soft: rgba(64, 140, 240, 0.14); - z-index: 1050; + z-index: 60; + box-sizing: border-box; width: min(320px, calc(100vw - 24px)); + min-width: min(200px, calc(100vw - 24px)); + max-width: calc(100vw - 24px); padding: 20px; - border: 1px solid var(--cr-border); - border-radius: 16px; + border: 1px solid rgba(17, 24, 39, 0.06); + border-radius: 18px; background: var(--cr-surface); box-shadow: - 0 24px 70px rgba(21, 26, 34, 0.24), - 0 2px 8px rgba(21, 26, 34, 0.06); + 0 24px 60px rgba(17, 24, 39, 0.16), + 0 6px 18px rgba(17, 24, 39, 0.06); color: var(--cr-text); font-family: 'Context Room Inter', 'HarmonyOS Sans SC', 'PingFang SC', sans-serif; } @@ -3714,9 +3717,10 @@ .context-room-document-create-popover input { width: 100%; height: 40px; - padding: 0 12px; + padding: 0 14px; border: 1px solid var(--cr-border); - border-radius: 10px; + border-radius: 12px; + line-height: 1; outline: none; background: var(--cr-surface-secondary); color: var(--cr-text); @@ -3744,7 +3748,7 @@ gap: 7px; margin-top: 12px; border: 1px solid var(--cr-border); - border-radius: 10px; + border-radius: 12px; background: var(--cr-surface); color: var(--cr-blue); font-size: 12.5px; @@ -3786,11 +3790,12 @@ } .context-room-document-create-popover footer button { - min-width: 64px; - height: 34px; - padding: 0 14px; + min-width: 72px; + height: 38px; + padding: 0 16px; border: 1px solid var(--cr-border); - border-radius: 9px; + border-radius: 10px; + line-height: 1; background: var(--cr-surface); color: var(--cr-muted); font-size: 12.5px; @@ -3894,30 +3899,45 @@ .context-room-artifact-toolbar { display: flex; flex: 0 0 auto; + flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; - padding: 10px 10px 8px; + row-gap: 8px; + padding: 12px 12px 10px; border-bottom: 1px solid var(--cr-border); } +/* 工具条右侧动作组:整体不可压缩,窄面板时整组换行而不是把「新建产物」挤成竖排。 */ +.context-room-artifact-toolbar-actions { + display: flex; + min-width: 0; + flex: 1 1 auto; + flex-wrap: wrap; + align-items: center; + justify-content: flex-end; + gap: 6px; + margin-left: auto; +} + /* 统一下拉筛选(FilterSelect):胶囊触发器 + 自绘菜单;选中非默认值转蓝。 */ .context-room-filter-select { display: inline-flex; - max-width: 180px; - height: 26px; + max-width: 190px; + height: 36px; align-items: center; - gap: 5px; - padding: 0 9px 0 12px; + gap: 6px; + padding: 0 12px 0 14px; border: 1px solid var(--cr-border); - border-radius: 999px; + border-radius: 10px; background: var(--cr-surface); color: var(--cr-muted); - font-size: 11px; + font-size: 12px; font-family: inherit; + line-height: 1; white-space: nowrap; cursor: pointer; - transition: border-color 0.15s ease, color 0.15s ease; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; } .context-room-filter-select:hover { @@ -3938,8 +3958,8 @@ .context-room-filter-select svg { flex: none; - width: 12px; - height: 12px; + width: 14px; + height: 14px; color: var(--cr-tertiary); } @@ -3956,22 +3976,26 @@ /* 下拉菜单弹层:与卡片菜单同底(card-menu 提供白底/阴影/变量域),补齐布局与选中态。 */ .context-room-filter-menu { --cr-blue: #3d6ff6; - min-width: 136px; - padding: 5px; - border: 1px solid var(--cr-border, #e5e7eb); - border-radius: 10px; + min-width: 150px; + padding: 7px; + border: 1px solid rgba(17, 24, 39, 0.06); + border-radius: 12px; + box-shadow: + 0 16px 40px rgba(17, 24, 39, 0.14), + 0 4px 12px rgba(17, 24, 39, 0.05); animation: context-room-filter-menu-in 0.16s ease; } .context-room-filter-menu-item { display: flex; + min-height: 36px; align-items: center; - gap: 8px; - padding: 6px 8px; - border-radius: 6px; + gap: 9px; + padding: 8px 10px; + border-radius: 8px; background: transparent; color: #1f1f1f; - font-size: 12px; + font-size: 12.5px; cursor: pointer; outline: none; user-select: none; @@ -4014,30 +4038,40 @@ .context-room-artifact-new { display: inline-flex; + min-width: 92px; + height: 36px; + flex: 0 0 auto; align-items: center; - gap: 4px; - padding: 3px 9px; + justify-content: center; + gap: 6px; + padding: 0 14px; border: 1px solid var(--cr-border); - border-radius: 6px; + border-radius: 10px; + background: var(--cr-surface); color: var(--cr-text); - font-size: 11px; + font-size: 12px; + font-weight: 500; + line-height: 1; + white-space: nowrap; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; } .context-room-artifact-new:hover { border-color: var(--cr-blue); + background: var(--cr-blue-soft); color: var(--cr-blue); } .context-room-artifact-new svg { - width: 12px; - height: 12px; + width: 14px; + height: 14px; } .context-room-artifact-list { min-height: 0; flex: 1; overflow-y: auto; - padding: 6px 8px 12px; + padding: 8px 10px 14px; } .context-room-artifact-row { @@ -4050,12 +4084,14 @@ display: flex; width: 100%; min-width: 0; + min-height: 44px; align-items: center; - gap: 9px; - padding: 8px 26px 8px 8px; - border-radius: 8px; + gap: 10px; + padding: 8px 34px 8px 10px; + border-radius: 10px; text-align: left; cursor: pointer; + transition: background-color 0.12s ease; } .context-room-artifact-item:hover { @@ -4082,19 +4118,19 @@ .context-room-artifact-ico { display: inline-flex; - width: 30px; - height: 30px; + width: 34px; + height: 34px; flex: 0 0 auto; align-items: center; justify-content: center; - border-radius: 7px; + border-radius: 9px; background: var(--cr-surface-sunken); color: var(--cr-muted); } .context-room-artifact-ico svg { - width: 15px; - height: 15px; + width: 16px; + height: 16px; } .context-room-artifact-body { @@ -4114,7 +4150,7 @@ .context-room-artifact-body b { color: var(--cr-text); - font-size: 12px; + font-size: 12.5px; font-weight: 500; } @@ -4131,7 +4167,7 @@ } .context-room-artifact-tag { - padding: 1px 6px; + padding: 2px 8px; border: 1px solid var(--cr-border); border-radius: 999px; color: var(--cr-muted); @@ -4147,9 +4183,9 @@ .context-room-artifact-acts { position: absolute; top: 50%; - right: 6px; + right: 8px; display: inline-flex; - gap: 2px; + gap: 4px; opacity: 0; transition: opacity 0.15s ease; } @@ -9144,6 +9180,7 @@ } .context-room-related-graph-canvas { + position: relative; height: auto; min-height: 0; min-width: 0; @@ -9155,6 +9192,42 @@ background-size: 24px 24px; } +/* 「自适应」浮层:视口可自由缩放平移,图缩小到看不见或拖走后, + 这颗按钮仍钉在画布右上角,一键把节点整体缩放居中回来。 + Context Room 各力导向图谱(首页全图/Room 关系/实体事实/建联/Wiki)共用同一套样式。 */ +.context-room-graph-fit { + position: absolute; + top: 10px; + right: 10px; + z-index: 2; + display: inline-flex; + height: 26px; + align-items: center; + gap: 5px; + padding: 0 9px; + border: 1px solid var(--cr-border); + border-radius: 6px; + background: color-mix(in srgb, var(--cr-surface) 92%, transparent); + color: var(--cr-muted); + font-size: 11px; + line-height: 1; + white-space: nowrap; + box-shadow: 0 1px 4px rgb(15 23 42 / 8%); + backdrop-filter: blur(4px); +} + +.context-room-graph-fit:hover { + border-color: var(--cr-border-strong); + background: var(--cr-surface); + color: var(--cr-text); +} + +.context-room-graph-fit svg { + width: 13px; + height: 13px; + flex: 0 0 auto; +} + .context-room-related-rooms-pane > .context-room-room-graph-inspector { height: auto; min-height: 0; @@ -15338,6 +15411,7 @@ button.eg-walk.is-next:hover { /* 新建产物:类型选择网格(轻文档 / Word / PPT / Excel)。 */ .context-room-document-create-popover.is-chooser { width: min(400px, calc(100vw - 24px)); + min-width: min(300px, calc(100vw - 24px)); } .context-room-document-create-types { @@ -15351,9 +15425,9 @@ button.eg-walk.is-next:hover { align-items: center; gap: 10px; min-width: 0; - padding: 12px 14px; + padding: 13px 15px; border: 1px solid var(--cr-border); - border-radius: 10px; + border-radius: 12px; background: var(--cr-surface); color: var(--cr-text); cursor: pointer; @@ -15372,8 +15446,8 @@ button.eg-walk.is-next:hover { } .context-room-document-create-type > svg { - width: 18px; - height: 18px; + width: 20px; + height: 20px; flex: none; color: var(--cr-blue); } @@ -15388,6 +15462,7 @@ button.eg-walk.is-next:hover { .context-room-document-create-type-body b { font-size: 13px; font-weight: 600; + white-space: nowrap; } .context-room-document-create-type-body small { @@ -15805,3 +15880,364 @@ button.eg-walk.is-next:hover { right: 0; text-align: center; } + +/* ── 产物行 ⋯ 操作菜单(移动到其他 Room / 删除) ── */ +.context-room-artifact-acts .context-room-resource-more { + display: inline-flex; + width: 26px; + height: 26px; + align-items: center; + justify-content: center; + border-radius: 8px; + color: var(--cr-muted); + opacity: 1; + transition: + background-color 120ms ease, + color 120ms ease; +} + +.context-room-artifact-acts .context-room-resource-more:hover:not(:disabled), +.context-room-artifact-acts .context-room-resource-more:focus-visible { + background: var(--cr-surface-hover); + color: var(--cr-text); +} + +.context-room-artifact-acts .context-room-resource-more svg { + width: 16px; + height: 16px; +} + +/* ── 移动到其他 Room 目标选择器 ── */ +.context-room-move-dialog { + display: flex; + width: min(480px, 100%); + max-width: 100%; + flex-direction: column; + gap: 14px; + margin: 0 auto; + padding: 22px 20px 20px; +} + +.context-room-move-dialog header { + display: flex; + align-items: flex-start; + gap: 12px; + padding-right: 44px; +} + +.context-room-move-dialog h2 { + margin: 0; + color: var(--cr-text); + font-size: 16px; + font-weight: 600; + line-height: 1.35; +} + +.context-room-move-dialog header p { + margin: 3px 0 0; + color: var(--cr-muted); + font-size: 12px; + line-height: 1.5; +} + +.context-room-move-symbol { + display: inline-flex; + width: 40px; + height: 40px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + border-radius: 12px; + background: var(--cr-blue-soft); + color: var(--cr-blue); +} + +.context-room-move-symbol svg { + width: 19px; + height: 19px; +} + +.context-room-move-search { + display: flex; + height: 40px; + align-items: center; + gap: 9px; + padding: 0 13px; + border: 1px solid var(--cr-border); + border-radius: 12px; + background: var(--cr-surface); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.context-room-move-search:focus-within { + border-color: var(--cr-blue); + box-shadow: 0 0 0 3px var(--cr-blue-soft); +} + +.context-room-move-search svg { + width: 15px; + height: 15px; + flex: 0 0 auto; + color: var(--cr-tertiary); +} + +.context-room-move-search input { + width: 100%; + min-width: 0; + border: 0; + outline: none; + background: transparent; + color: var(--cr-text); + font-size: 13px; + line-height: 1; +} + +.context-room-move-list { + display: flex; + max-height: min(320px, 46vh); + flex-direction: column; + gap: 2px; + overflow-y: auto; + padding: 6px; + border: 1px solid var(--cr-border); + border-radius: 12px; + background: var(--cr-surface); +} + +.context-room-move-group { + display: flex; + align-items: center; + gap: 6px; + margin: 8px 8px 4px; + color: var(--cr-tertiary); + font-size: 11px; +} + +.context-room-move-group svg { + width: 11px; + height: 11px; +} + +.context-room-move-room { + display: flex; + width: 100%; + min-height: 40px; + align-items: center; + padding: 9px 12px; + border-radius: 9px; + text-align: left; + transition: background-color 0.12s ease; +} + +.context-room-move-room:hover { + background: var(--cr-surface-hover); +} + +.context-room-move-room[data-selected] { + background: var(--cr-blue-soft); +} + +.context-room-move-room-body { + display: flex; + min-width: 0; + flex-direction: column; + gap: 1px; +} + +.context-room-move-room-body b { + overflow: hidden; + color: var(--cr-text); + font-size: 13px; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} + +.context-room-move-room[data-selected] .context-room-move-room-body b { + color: var(--cr-blue); +} + +.context-room-move-room-body small { + color: var(--cr-tertiary); + font-size: 11px; +} + +.context-room-move-hint { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + padding: 10px; + color: var(--cr-tertiary); + font-size: 12px; +} + +.context-room-move-hint svg { + width: 13px; + height: 13px; +} + +.context-room-move-error { + margin: 0; + color: #b54732; + font-size: 11px; + line-height: 1.5; +} + +.context-room-move-dialog footer { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.context-room-action-confirm-note { + margin: 0; + color: var(--cr-tertiary); + font-size: 11px; + line-height: 1.5; +} + +/* 产物相关的弹窗:16px 大圆角 + 柔和分层阴影(rounded-2xl 档)。 */ +.context-room-dialog-content.context-room-move-dialog-shell { + width: min(520px, calc(100vw - 32px)); + border: 1px solid rgba(17, 24, 39, 0.06); + border-radius: 16px; + box-shadow: + 0 24px 60px rgba(17, 24, 39, 0.16), + 0 6px 18px rgba(17, 24, 39, 0.06); +} + +.context-room-dialog-content.context-room-artifact-delete-shell { + width: min(480px, calc(100vw - 32px)); + border: 1px solid rgba(17, 24, 39, 0.06); + border-radius: 16px; + box-shadow: + 0 24px 60px rgba(17, 24, 39, 0.16), + 0 6px 18px rgba(17, 24, 39, 0.06); +} + +.context-room-artifact-delete-shell .context-room-action-confirm { + width: 100%; +} + +/* 弹窗底部按钮统一 40px 高、12px 圆角,图标/文字垂直居中。 */ +.context-room-move-dialog footer button, +.context-room-artifact-delete-shell .context-room-action-confirm footer button { + min-height: 40px; + padding: 0 16px; + border-radius: 12px; + font-size: 13px; + line-height: 1; +} + +/* ── 产物库多选整理 ── */ +.context-room-artifact-select-toggle, +.context-room-artifact-selection-move, +.context-room-artifact-selection-cancel { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 6px; + height: 36px; + padding: 0 13px; + border: 1px solid var(--cr-border); + border-radius: 10px; + background: var(--cr-surface); + color: var(--cr-text); + font-size: 12px; + font-weight: 500; + line-height: 1; + white-space: nowrap; + transition: border-color 0.15s ease, color 0.15s ease, background-color 0.15s ease; +} + +.context-room-artifact-select-toggle:hover, +.context-room-artifact-selection-cancel:hover { + border-color: var(--cr-blue); + color: var(--cr-blue); +} + +.context-room-artifact-select-toggle svg, +.context-room-artifact-selection-move svg { + width: 14px; + height: 14px; +} + +.context-room-artifact-selection { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 6px; +} + +.context-room-artifact-selection > span { + color: var(--cr-muted); + font-size: 12px; + white-space: nowrap; +} + +.context-room-artifact-selection-move { + border-color: var(--cr-blue); + background: var(--cr-blue-soft); + color: var(--cr-blue); +} + +.context-room-artifact-selection-move:disabled { + border-color: var(--cr-border); + background: transparent; + color: var(--cr-tertiary); + cursor: not-allowed; +} + +.context-room-artifact-check { + display: inline-flex; + width: 18px; + height: 18px; + flex: 0 0 auto; + align-items: center; + justify-content: center; + margin-left: 6px; + border: 1px solid var(--cr-border); + border-radius: 5px; + background: var(--cr-surface); + color: #fff; +} + +.context-room-artifact-check[aria-checked='true'] { + border-color: var(--cr-blue); + background: var(--cr-blue); +} + +.context-room-artifact-check svg { + width: 12px; + height: 12px; +} + +.context-room-artifact-row[data-selecting] .context-room-artifact-item { + padding-left: 4px; +} + +/* 产物行 ⋯ 菜单:12px 圆角 + 柔和阴影 + 36px 行高,与浮层/弹窗同一视觉档。 */ +.context-room-artifact-menu { + z-index: 60; + min-width: 200px; + padding: 8px; + border-color: rgba(17, 24, 39, 0.06); + border-radius: 12px; + box-shadow: + 0 16px 40px rgba(17, 24, 39, 0.14), + 0 4px 12px rgba(17, 24, 39, 0.05); +} + +.context-room-artifact-menu [role='menuitem'] { + min-height: 36px; + gap: 9px; + padding: 8px 10px; + border-radius: 8px; + font-size: 12.5px; +} + +.context-room-artifact-menu svg { + width: 15px; + height: 15px; +} diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/components/EntityFactGraphCanvas.tsx b/apps/desktop/src/renderer/src/components/context-room/ported/components/EntityFactGraphCanvas.tsx index cbbbe589..a4de7302 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/components/EntityFactGraphCanvas.tsx +++ b/apps/desktop/src/renderer/src/components/context-room/ported/components/EntityFactGraphCanvas.tsx @@ -11,6 +11,7 @@ import { useForceGraphLayout, } from '@/components/graph' import { useLocale } from '../../../../i18n/LocaleContext' +import { GraphFitButton } from './GraphFitButton' import type { EntityFactGraphData, EntityFactGraphNode } from './entityFactGraphModel' /** 详情面板小视口:布局稳定后只居中、不缩小;收敛期间相机逐帧跟随内容。 */ @@ -158,6 +159,7 @@ export function EntityFactGraphCanvas({ data, onSelect, selectedId }: EntityFact onReleaseNode={layout.release} onSelectNode={onSelect} /> + {data.nodes.length > 0 ? canvasRef.current?.fitView()} /> : null}
{data.nodes.map((node: EntityFactGraphNode) => ( + ) +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/components/HomeView.tsx b/apps/desktop/src/renderer/src/components/context-room/ported/components/HomeView.tsx index cbde5a8b..f92df9d9 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/components/HomeView.tsx +++ b/apps/desktop/src/renderer/src/components/context-room/ported/components/HomeView.tsx @@ -7,7 +7,7 @@ import { Search, Trash2, } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useLocale } from '../../../../i18n/LocaleContext'; import type { ContextRoomRecord } from '../types'; @@ -18,7 +18,8 @@ import { RoomCard } from './RoomCard'; import { RoomCreationStudio } from './RoomCreationStudio'; import { RoomLifecycleDialogs } from './RoomDialogs'; import { isMergeRecommendationCandidate, RoomDuplicateCenter } from './RoomDuplicateCenter'; -import { RoomGraphCanvas } from './RoomGraphCanvas'; +import { GraphFitButton } from './GraphFitButton'; +import { RoomGraphCanvas, type RoomGraphCanvasHandle } from './RoomGraphCanvas'; import { RoomNodeInspector } from './RoomGraphInspector'; import { RoomRelationInspector } from './RoomRelationControls'; import { useRoomRelationGraph } from '../hooks/useRoomRelationGraph'; @@ -37,9 +38,11 @@ function RoomGraph({ const { error, graph, loading, reload } = useRoomRelationGraph(null); const selected = rooms.find((room) => room.id === selectedId); const selectedRelation = (graph?.edges ?? []).find((edge) => edge.id === selectedRelationId) ?? null; + const graphRef = useRef(null); // 首页图谱对齐原型:无工具栏(搜索/筛选/关系管理收进 Room 详情的关系面板), - // 只保留标题 + 画布 + 点选节点/关系出检查器。 + // 只保留标题 + 画布 + 点选节点/关系出检查器;另加一颗常驻画布的自适应按钮, + // 因为视口能自由缩放平移,缩到最小或拖走后整张图会看不见。 return (
@@ -49,6 +52,7 @@ function RoomGraph({
{loading && !graph ?
{t('contextRoom:relations.loadingGraph')}
: ( )} + {graph ? void graphRef.current?.fitView()} /> : null} {error ?
{t('contextRoom:relations.degradedNoSyntheticEdges')}
: null}
{selectedRelation ? ( diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/components/LinkGraphCanvas.tsx b/apps/desktop/src/renderer/src/components/context-room/ported/components/LinkGraphCanvas.tsx index 04d8d192..2cdd2035 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/components/LinkGraphCanvas.tsx +++ b/apps/desktop/src/renderer/src/components/context-room/ported/components/LinkGraphCanvas.tsx @@ -12,6 +12,7 @@ import { type ForceGraphEdge, } from '@/components/graph' import { useLocale } from '../../../../i18n/LocaleContext' +import { GraphFitButton } from './GraphFitButton' import { LINK_GRAPH_ROOT_ID, linkGraphBlockNodeId, @@ -353,6 +354,7 @@ export function LinkGraphCanvas({ onReleaseNode={layout.release} onSelectNode={onSelect} /> + {data.nodes.length > 0 ? canvasRef.current?.fitView()} /> : null}
{data.nodes.map((node: LinkGraphNode) => ( + ) + + return ( + +
+
+ + +
+

{t('contextRoom:moveDocument.title')}

+

+ {isBatch + ? t('contextRoom:moveDocument.subtitleBatch', { count: documents.length }) + : t('contextRoom:moveDocument.subtitle', { name: documents[0]?.title ?? '' })} +

+
+
+ + + +
+ {loading ? ( +

+

+ ) : null} + {loadError ?

{loadError}

: null} + {!loading && !loadError && recentRooms.length > 0 ? ( +

+

+ ) : null} + {!loading && !loadError ? recentRooms.map(renderRoom) : null} + {!loading && !loadError && otherRooms.length > 0 && recentRooms.length > 0 ? ( +

{t('contextRoom:moveDocument.allRooms')}

+ ) : null} + {!loading && !loadError ? otherRooms.map(renderRoom) : null} + {!loading && !loadError && matches.length === 0 ? ( +

{t('contextRoom:moveDocument.noMatch')}

+ ) : null} +
+ + {moveError ?

{moveError}

: null} + +
+ + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/components/WikiGraphCanvas.tsx b/apps/desktop/src/renderer/src/components/context-room/ported/components/WikiGraphCanvas.tsx index af2f32ff..bfd5a404 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/components/WikiGraphCanvas.tsx +++ b/apps/desktop/src/renderer/src/components/context-room/ported/components/WikiGraphCanvas.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef } from 'react' +import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' import type { KnowledgeWikiGraphDto } from '../../../../../../shared/knowledge' import { @@ -10,6 +10,7 @@ import { useForceGraphLayout, } from '@/components/graph' import { useLocale } from '../../../../i18n/LocaleContext' +import { GraphFitButton } from './GraphFitButton' /** 详情面板小视口:布局稳定后只居中、不缩小;收敛期间相机逐帧跟随内容。 */ const SETTLE_FIT = { minScale: 1, follow: true } @@ -60,12 +61,28 @@ function initialPositions(count: number, width: number, height: number) { return positions } -/** Wiki 内链图谱:Worker 计算 d3-force,画布只读取 SharedArrayBuffer 坐标。 */ -export function WikiGraphCanvas({ graph, selectedPath, onSelectPage }: { +export interface WikiGraphCanvasHandle { + fitView(): void +} + +interface WikiGraphCanvasProps { graph: KnowledgeWikiGraphDto selectedPath: string | null onSelectPage: (path: string) => void -}) { + /** + * 是否在画布右上角挂内置的自适应浮层。宿主自己有图谱工具栏时关掉, + * 改成在工具栏里放一颗,避免和工具栏的按钮叠在一起。 + */ + showFitButton?: boolean +} + +/** Wiki 内链图谱:Worker 计算 d3-force,画布只读取 SharedArrayBuffer 坐标。 */ +function WikiGraphCanvasComponent({ + graph, + selectedPath, + onSelectPage, + showFitButton = true, +}: WikiGraphCanvasProps, ref: React.ForwardedRef) { const { t } = useLocale() const canvasRef = useRef(null) const nodeIndex = useMemo( @@ -120,6 +137,11 @@ export function WikiGraphCanvas({ graph, selectedPath, onSelectPage }: { const selectedId = selectedPath ? graph.nodes.find((node) => node.path === selectedPath)?.id ?? null : null + useImperativeHandle(ref, () => ({ + fitView() { + canvasRef.current?.fitView() + }, + }), []) return (
@@ -144,6 +166,9 @@ export function WikiGraphCanvas({ graph, selectedPath, onSelectPage }: { if (path) onSelectPage(path) }} /> + {showFitButton && graph.nodes.length > 0 + ? canvasRef.current?.fitView()} /> + : null}
{graph.nodes.map((node) => ( + +
+ ) : ( - - - - {createType === null ? ( -
- {CREATE_TYPES.map(({ id, label, hint, icon: Icon }) => ( - - ))} -
- ) : createType === 'doc' ? ( -
{ event.preventDefault(); void createDocument(); }}> - - setNewDocumentTitle(event.target.value)} - disabled={creatingDocument} - /> - { - const file = event.currentTarget.files?.[0]; - event.currentTarget.value = ''; - if (file) void importMarkdownDocument(file); - }} - /> - - {createError ? {createError} : null} -
- + + + + {createType === null ? ( +
+ {CREATE_TYPES.map(({ id, label, hint, icon: Icon }) => ( + + ))} +
+ ) : createType === 'doc' ? ( + { event.preventDefault(); void createDocument(); }}> + + setNewDocumentTitle(event.target.value)} + disabled={creatingDocument} + /> + { + const file = event.currentTarget.files?.[0]; + event.currentTarget.value = ''; + if (file) void importMarkdownDocument(file); + }} + /> + - + +
+
+ ) : ( +
{ event.preventDefault(); dispatchOfficeCreate(createType); }}> + + setNewDocumentTitle(event.target.value)} + /> + { + const files = Array.from(event.currentTarget.files ?? []); + event.currentTarget.value = ''; + if (files.length > 0) void importOfficeFiles(files); + }} + /> + - -
- ) : ( -
{ event.preventDefault(); dispatchOfficeCreate(createType); }}> - - setNewDocumentTitle(event.target.value)} - /> - { - const files = Array.from(event.currentTarget.files ?? []); - event.currentTarget.value = ''; - if (files.length > 0) void importOfficeFiles(files); - }} - /> - - {createError ? {createError} : null} - {t('contextRoom:artifactLibrary.officeCreateHint')} -
- - -
-
- )} - -
-
- + {createError ? {createError} : null} + {t('contextRoom:artifactLibrary.officeCreateHint')} +
+ + +
+ + )} + + + + +
+ 0} + onOpenChange={(open) => { + if (!open) setMoveTargets([]); + }} + documents={moveTargets} + onMoved={() => exitSelection()} + /> + { + if (!open && deletingArtifact) return; + if (!open) { + setPendingDelete(null); + setDeleteError(null); + } + }} + title={pendingDelete + ? pendingDelete.kind === 'file' + ? t('contextRoom:artifactLibrary.confirmDeleteFileName', { name: pendingDelete.name }) + : t('contextRoom:resource.confirmMovingDocumentNameToTrash', { name: pendingDelete.name }) + : ''} + contentClassName="context-room-artifact-delete-shell" + > +
+
+

{pendingDelete?.kind === 'file' + ? t('contextRoom:artifactLibrary.deleteFile') + : t('contextRoom:resource.moveToTrash')}

+
+
+

{pendingDelete?.kind === 'file' + ? t('contextRoom:artifactLibrary.confirmDeleteFile') + : t('contextRoom:resource.confirmMoveToTrash')}

+

+ {pendingDelete + ? pendingDelete.kind === 'file' + ? t('contextRoom:artifactLibrary.deleteFileIrreversible') + : t('contextRoom:resource.nameCanBeRestoredFromTrash', { name: pendingDelete.name }) + : null} +

+ {deleteError ?

{deleteError}

: null} +
+
+ + +
+
+
{visibleArtifacts.map(({ key, resource }) => { const office = resource.kind === 'knowledge-file' ? resource : null; @@ -414,12 +536,34 @@ export function ArtifactLibraryPane({ const backendDocument = cloud ? backendDocuments.find((document) => document.id === cloud.binding.docId) ?? null : null; const citations = backendDocument ? citationCounts.get(backendDocument.id) ?? 0 : 0; return ( -
+
+ {selectionMode && backendDocument ? ( + + ) : null} {office || backendDocument ? ( - {office ? ( - - ) : null} - { - if (!open && deletingArtifact && pendingDelete?.key === key) return; - setDeleteError(null); - setPendingDelete(open - ? office - ? { key, kind: 'file', fileId: office.fileId, name: resource.name } - : backendDocument - ? { key, kind: 'document', document: backendDocument, name: resource.name } - : null - : null); - }} - > - + + - - - + + -

{office ? t('contextRoom:artifactLibrary.confirmDeleteFile') : t('contextRoom:resource.confirmMoveToTrash')}

- {office - ? t('contextRoom:artifactLibrary.deleteFileIrreversible') - : t('contextRoom:resource.nameCanBeRestoredFromTrash', { name: resource.name })} - {deleteError ? {deleteError} : null} -
- - - - -
- -
-
-
+
) : null}
diff --git a/apps/desktop/src/renderer/src/components/context-room/ported/components/detail-panels/RelationsPane.tsx b/apps/desktop/src/renderer/src/components/context-room/ported/components/detail-panels/RelationsPane.tsx index e43e1c39..ce9e66ff 100644 --- a/apps/desktop/src/renderer/src/components/context-room/ported/components/detail-panels/RelationsPane.tsx +++ b/apps/desktop/src/renderer/src/components/context-room/ported/components/detail-panels/RelationsPane.tsx @@ -1,4 +1,4 @@ -import { Link2, Maximize2, Network } from 'lucide-react' +import { Link2, Network } from 'lucide-react' import { useEffect, useMemo, useRef, useState } from 'react' import { useLocale } from '../../../../../i18n/LocaleContext' import type { RoomDocument } from '@nxcore/agent-contract' @@ -10,6 +10,7 @@ import { useRoomRelationGraph } from '../../hooks/useRoomRelationGraph' import { RoomGraphCanvas, type RoomGraphCanvasHandle } from '../RoomGraphCanvas' import { RoomNodeInspector } from '../RoomGraphInspector' import { CreateRoomRelationDialog, relationTypeLabel, RoomRelationInspector } from '../RoomRelationControls' +import { GraphFitButton } from '../GraphFitButton' import { PanelEmptyState } from './PanelEmptyState' export function RelationsPane({ @@ -72,22 +73,26 @@ export function RelationsPane({ {t(visibility === 'hidden' ? 'contextRoom:relations.showActive' : 'contextRoom:relations.showHidden')} -
{loading && !graph ?
{t('contextRoom:relations.loadingGraph')}
: hasRelations ? ( - { setSelectedRelationId(null); setSelectedGraphRoomId(roomId ?? room.id); setInspectorOpen(Boolean(roomId)) }} - onSelectRelation={(relationId) => { setSelectedRelationId(relationId); setSelectedGraphRoomId(room.id); setInspectorOpen(Boolean(relationId)) }} - onOpenRoom={onOpenRoom} - /> + <> + { setSelectedRelationId(null); setSelectedGraphRoomId(roomId ?? room.id); setInspectorOpen(Boolean(roomId)) }} + onSelectRelation={(relationId) => { setSelectedRelationId(relationId); setSelectedGraphRoomId(room.id); setInspectorOpen(Boolean(relationId)) }} + onOpenRoom={onOpenRoom} + /> + {/* 视口可自由缩放平移,缩到最小或拖出视野后图会看不见; + 浮层按钮常驻画布右上角,一键把图谱自适应缩放并居中。 */} + void graphRef.current?.fitView()} /> + ) : ( ([ 'document.changed', 'document.operation.changed', 'document.deleted', + 'document.moved', ]) export function useRoomDocuments(roomIds: string[]) { @@ -105,6 +106,30 @@ export function useRoomDocuments(roomIds: string[]) { const unsubscribeEvents = documents.onEvent(({ event }) => { if (!DOCUMENT_STATE_EVENT_TYPES.has(event.type)) return + // 跨 Room 移动:事件发往源房,先摘除再拉一次目标房基线(目标房另有 + // document.changed 增量,重复合并按 id 去重,幂等)。 + if (event.type === 'document.moved') { + const payload = event.payload && typeof event.payload === 'object' + ? event.payload as Partial + : null + setDocumentsByRoom((current) => ({ + ...current, + [event.roomId]: (current[event.roomId] ?? []).filter((candidate) => candidate.id !== event.documentId), + })) + setTrashedDocumentsByRoom((current) => ({ + ...current, + [event.roomId]: (current[event.roomId] ?? []).filter((candidate) => candidate.id !== event.documentId), + })) + setFocusedDocumentByRoom((current) => current[event.roomId] === event.documentId + ? { ...current, [event.roomId]: null } + : current) + invalidateDocumentBlockReferences({ roomId: event.roomId, documentId: event.documentId }) + if (typeof payload?.toRoomId === 'string' && payload.toRoomId) { + void refreshRoom(payload.toRoomId).catch(() => undefined) + } + return + } + const document = documentFromEvent(event) if (document) { upsertDocument(document) diff --git a/apps/desktop/src/renderer/src/components/pages/AgentConnectionsPage.css b/apps/desktop/src/renderer/src/components/pages/AgentConnectionsPage.css index 993e60b4..42465dc3 100644 --- a/apps/desktop/src/renderer/src/components/pages/AgentConnectionsPage.css +++ b/apps/desktop/src/renderer/src/components/pages/AgentConnectionsPage.css @@ -1,104 +1,197 @@ /* Agent 连接页:布局与页面特有样式。控件基底复用全局类 - * (.page/.page-header/.primary-button/.secondary-button/.icon-button), - * 表单控件配方与 AgentSchedulesPage / source-connect-form 对齐。 */ + * (.page / .page-header / .primary-button / .secondary-button / .icon-button), + * 本页把它们统一抬到 40px 高、10px 圆角,图标与文字垂直居中; + * 面板用大圆角 + 柔和分层阴影,间距留白拉开 —— 现代、干净、有呼吸感。 */ .agent-connections-page { + /* 全页公共度量:改这两个值即可整体收放控件与面板 */ + --ac-control-height: 40px; + --ac-control-radius: 10px; + --ac-panel-radius: 16px; + --ac-shadow-card: 0 1px 2px rgba(21, 26, 34, 0.04), 0 10px 26px -20px rgba(21, 26, 34, 0.2); + --ac-shadow-float: 0 2px 8px rgba(21, 26, 34, 0.05), 0 32px 64px -36px rgba(21, 26, 34, 0.32); display: flex; flex-direction: column; - gap: 14px; - max-width: 980px; + gap: 20px; + max-width: 1000px; +} + +/* 页头与正文用统一节奏:间距全部交给 .agent-connections-page 的 gap */ +.agent-connections-page .page-header { + margin-bottom: 0; +} + +.agent-connections-page .page-header-actions { + align-items: center; +} + +/* ── 控件基底:本页所有按钮/输入都 40px 高、10px 圆角、内容垂直居中 ── */ + +.agent-connections-page :is(.primary-button, .secondary-button) { + min-height: var(--ac-control-height); + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 14px; + border-radius: var(--ac-control-radius); + font-size: 12.5px; + line-height: 1; + white-space: nowrap; + transition: background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease; +} + +.agent-connections-page :is(.primary-button, .secondary-button) svg { + width: 15px; + height: 15px; + flex: 0 0 auto; +} + +.agent-connections-page .secondary-button:hover { + border-color: var(--border-strong); + background: var(--surface-alt); +} + +.agent-connections-page .icon-button { + width: var(--ac-control-height); + height: var(--ac-control-height); + flex: 0 0 var(--ac-control-height); + border: 1px solid var(--border); + border-radius: var(--ac-control-radius); + background: var(--surface); +} + +.agent-connections-page .icon-button:hover { + border-color: var(--border-strong); + background: var(--surface-alt); } .agent-connections-subtitle { display: flex; align-items: center; - gap: 7px; - margin: -6px 0 4px; + gap: 8px; + margin: -6px 0 0; color: var(--muted); font-size: 12px; + line-height: 1.5; } .agent-connections-subtitle svg { width: 14px; height: 14px; + flex: 0 0 auto; } .agent-connections-error { - border: 1px solid color-mix(in srgb, #b5493a 35%, var(--border)); - border-radius: 6px; - padding: 11px 14px; + border: 1px solid color-mix(in srgb, #b5493a 32%, var(--border)); + border-radius: var(--ac-control-radius); + background: color-mix(in srgb, #b5493a 6%, var(--surface)); + padding: 12px 16px; font-size: 12px; + line-height: 1.5; color: #b5493a; } -/* 固定端口设置卡 */ +/* ── 固定端口设置卡 ── */ + .agent-connections-port { display: flex; + align-items: center; justify-content: space-between; - gap: 16px; + gap: 24px; border: 1px solid var(--border); - border-radius: 8px; + border-radius: var(--ac-panel-radius); background: var(--surface); - padding: 14px 18px; + box-shadow: var(--ac-shadow-card); + padding: 20px 22px; } .agent-connections-port-copy h2 { display: flex; align-items: center; - gap: 6px; + gap: 8px; font-size: 14px; - margin: 0 0 4px; + margin: 0 0 6px; } .agent-connections-port-copy h2 svg { width: 15px; height: 15px; + flex: 0 0 auto; } .agent-connections-port-copy p { - margin: 0 0 4px; + margin: 0 0 8px; color: var(--muted); - font-size: 11px; + font-size: 11.5px; + line-height: 1.6; max-width: 560px; } .agent-connections-port-copy small { color: var(--subtle); - font-size: 10px; + font-size: 10.5px; + line-height: 1.5; } .agent-connections-port-controls { display: flex; align-items: center; - gap: 8px; + gap: 10px; flex-wrap: wrap; justify-content: flex-end; - max-width: 360px; + max-width: 380px; } .agent-connections-port-controls input { - width: 96px; + width: 110px; + box-sizing: border-box; + height: var(--ac-control-height); + /* 等高 + 零纵向 padding:占位符与输入值都严格垂直居中 */ + padding: 0 12px; + border: 1px solid var(--border); + border-radius: var(--ac-control-radius); + background: var(--surface); + color: var(--text); + font: inherit; + font-size: 12.5px; + line-height: calc(var(--ac-control-height) - 2px); + transition: border-color 0.16s ease, box-shadow 0.16s ease; +} + +.agent-connections-port-controls input::placeholder { + color: var(--subtle); +} + +.agent-connections-port-controls input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); } .agent-connections-port-message { flex-basis: 100%; text-align: right; color: var(--muted); - font-size: 10px; + font-size: 10.5px; + line-height: 1.5; } -/* 新建表单(配方同 schedule-form) */ +/* ── 新建连接表单:当作一颗浮起的面板(大圆角 + 柔和阴影) ── */ + .agent-connections-form { - border: 1px solid var(--border-strong); + border: 1px solid var(--border); + border-radius: 20px; background: var(--surface); - padding: 18px; + box-shadow: var(--ac-shadow-float); + padding: 24px; } .agent-connections-form-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + gap: 12px; + margin-bottom: 20px; } .agent-connections-form-header h2 { @@ -109,29 +202,33 @@ .agent-connections-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; + gap: 16px; } .agent-connections-form-grid label { display: grid; - gap: 6px; + gap: 8px; color: var(--muted); - font-size: 11px; + font-size: 11.5px; + line-height: 1.4; } .agent-connections-form-grid fieldset { display: grid; - gap: 6px; + gap: 8px; margin: 0; border: 1px solid var(--border); - border-radius: 6px; - padding: 10px; - font-size: 11px; + border-radius: 14px; + background: var(--surface-alt); + padding: 14px; + font-size: 11.5px; color: var(--muted); } .agent-connections-form-grid legend { - padding: 0 4px; + padding: 0 6px; + color: var(--muted); + font-size: 11px; } .agent-connections-form-grid input:not([type='checkbox']):not([type='radio']), @@ -139,17 +236,48 @@ .agent-connections-form-grid textarea { width: 100%; box-sizing: border-box; - padding: 8px 9px; border: 1px solid var(--border); - border-radius: 6px; - background: var(--background); + border-radius: var(--ac-control-radius); + background: var(--surface); color: var(--text); font: inherit; - font-size: 12px; + font-size: 12.5px; + transition: border-color 0.16s ease, box-shadow 0.16s ease; +} + +.agent-connections-form-grid input:not([type='checkbox']):not([type='radio']), +.agent-connections-form-grid select { + height: var(--ac-control-height); + /* 等高 + 零纵向 padding:文字与占位符都严格垂直居中 */ + padding: 0 12px; + line-height: calc(var(--ac-control-height) - 2px); +} + +.agent-connections-form-grid textarea { + min-height: 96px; + padding: 10px 12px; + line-height: 1.6; + resize: vertical; +} + +.agent-connections-form-grid input::placeholder, +.agent-connections-form-grid textarea::placeholder { + color: var(--subtle); +} + +.agent-connections-form-grid input:not([type='checkbox']):not([type='radio']):focus, +.agent-connections-form-grid select:focus, +.agent-connections-form-grid textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); } .agent-connections-form-grid input[type='checkbox'], .agent-connections-form-grid input[type='radio'] { + width: 15px; + height: 15px; + flex: 0 0 15px; accent-color: var(--accent); margin: 0; } @@ -161,20 +289,31 @@ .agent-connections-check { display: flex; align-items: center; - gap: 6px; - font-size: 12px; + gap: 8px; + min-height: 34px; + padding: 0 10px; + border-radius: 9px; color: var(--text); + font-size: 12px; + line-height: 1.4; + cursor: pointer; + transition: background-color 0.14s ease; +} + +.agent-connections-check:hover { + background: color-mix(in srgb, var(--hover) 70%, transparent); } .agent-connections-room-picker { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 4px; - max-height: 148px; + max-height: 168px; overflow-y: auto; border: 1px solid var(--border); - border-radius: 6px; - padding: 6px 8px; + border-radius: 12px; + background: var(--surface); + padding: 8px; } .agent-connections-capabilities { @@ -186,23 +325,35 @@ .agent-connections-form-actions { display: flex; justify-content: flex-end; - gap: 8px; - margin-top: 16px; + align-items: center; + gap: 10px; + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid var(--border); } -/* 连接卡片(版式同 schedule-card) */ +/* ── 连接卡片 ── */ + .agent-connections-list { display: grid; - gap: 10px; + gap: 14px; } .agent-connections-card { display: flex; flex-direction: column; - gap: 8px; + gap: 14px; border: 1px solid var(--border); + border-radius: var(--ac-panel-radius); background: var(--surface); - padding: 16px 18px; + box-shadow: var(--ac-shadow-card); + padding: 20px 22px; + transition: border-color 0.18s ease, box-shadow 0.18s ease; +} + +.agent-connections-card:hover { + border-color: var(--border-strong); + box-shadow: 0 2px 4px rgba(21, 26, 34, 0.04), 0 16px 36px -24px rgba(21, 26, 34, 0.26); } .agent-connections-card[data-status='revoked'] { @@ -212,54 +363,65 @@ .agent-connections-card-copy { display: flex; justify-content: space-between; - gap: 12px; + gap: 16px; } .agent-connections-card-title { display: flex; align-items: center; - gap: 8px; + gap: 10px; + flex-wrap: wrap; } .agent-connections-card-title h2 { - font-size: 14px; + font-size: 14.5px; margin: 0; } .agent-connections-card-copy p { - margin: 7px 0 5px; + margin: 9px 0 7px; color: var(--muted); - font-size: 11px; + font-size: 11.5px; + line-height: 1.6; } .agent-connections-card-copy small { color: var(--subtle); - font-size: 10px; + font-size: 10.5px; + line-height: 1.5; } .agent-connections-card-copy code { - padding: 2px 6px; + padding: 3px 8px; border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-alt); color: var(--muted); - font: 10px ui-monospace, monospace; + font: 10.5px ui-monospace, monospace; } -/* 徽标/状态(配方同 schedule-badge) */ +/* 徽标 / 状态胶囊 */ .agent-connections-badge { - padding: 2px 6px; + padding: 3px 9px; border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface-alt); color: var(--muted); - font-size: 10px; + font-size: 10.5px; + line-height: 1.5; } .agent-connections-status { - padding: 2px 6px; + padding: 3px 9px; border: 1px solid var(--border); - font-size: 10px; + border-radius: 999px; + font-size: 10.5px; + line-height: 1.5; } .agent-connections-status.status-active { - border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + border-color: color-mix(in srgb, var(--accent) 38%, var(--border)); + background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent); } @@ -274,67 +436,87 @@ .agent-connections-card-actions { display: flex; align-items: center; - gap: 6px; + gap: 8px; justify-content: flex-end; flex-wrap: wrap; + padding-top: 2px; } -/* 动作按钮:复用 .secondary-button 基底,仅做紧凑修饰 */ +/* 卡片内动作按钮:与页面级按钮同高(40px),只把字号与内边距收一档 */ .agent-connections-action { - min-height: 28px; - padding: 4px 10px; - gap: 5px; - font-size: 11px; + min-height: var(--ac-control-height); + padding: 0 13px; + gap: 7px; + font-size: 11.5px; + line-height: 1; } .agent-connections-action svg { - width: 13px; - height: 13px; + width: 14px; + height: 14px; + flex: 0 0 auto; } .agent-connections-action.danger { color: #b5493a; } -/* token 一次性展示 */ +.agent-connections-action.danger:hover { + border-color: color-mix(in srgb, #b5493a 40%, var(--border)); + background: color-mix(in srgb, #b5493a 6%, var(--surface)); +} + +/* ── token 一次性展示 ── */ + .agent-connections-token { display: flex; flex-direction: column; - gap: 4px; - border: 1px solid var(--border-strong); - border-radius: 6px; - padding: 8px 10px; - background: var(--background); + gap: 8px; + border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--border)); + border-radius: 14px; + padding: 16px 18px; + background: color-mix(in srgb, var(--accent) 4%, var(--surface)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6); } .agent-connections-token strong { - font-size: 12px; + font-size: 12.5px; } .agent-connections-token small { color: var(--muted); - font-size: 10px; + font-size: 10.5px; + line-height: 1.5; } .agent-connections-token-row { display: flex; align-items: center; - gap: 8px; + gap: 10px; + margin-top: 2px; } .agent-connections-token-row code { flex: 1; word-break: break-all; - font-size: 11px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: var(--ac-control-radius); + background: var(--surface); + font: 11.5px ui-monospace, monospace; + line-height: 1.5; } -/* 结果面板(自检/示例/安装/调试) */ +/* ── 结果面板(自检 / 示例 / 安装 / 调试) ── */ + .agent-connections-panel { display: flex; flex-direction: column; - gap: 6px; - border-top: 1px dashed var(--border); - padding-top: 8px; + gap: 10px; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface-alt); + padding: 16px 18px; } .agent-connections-panel h3 { @@ -343,6 +525,16 @@ color: var(--muted); } +.agent-connections-panel > div { + display: grid; + gap: 6px; +} + +.agent-connections-panel > strong { + font-size: 11.5px; + color: var(--muted); +} + .agent-connections-steps, .agent-connections-hits, .agent-connections-files, @@ -352,14 +544,15 @@ list-style: none; display: flex; flex-direction: column; - gap: 4px; + gap: 6px; font-size: 12px; + line-height: 1.5; } .agent-connections-steps li { display: flex; align-items: baseline; - gap: 8px; + gap: 10px; } .agent-connections-steps li[data-tone='ok'] code { color: #1c7d4d; } @@ -369,13 +562,16 @@ .agent-connections-hits li { display: flex; align-items: baseline; - gap: 8px; + gap: 10px; flex-wrap: wrap; + padding: 2px 0; } .agent-connections-hit-kind { - padding: 0 5px; + padding: 1px 7px; border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); font-size: 10px; color: var(--muted); } @@ -388,35 +584,76 @@ .agent-connections-panel pre { margin: 0; - padding: 8px 10px; + padding: 12px 14px; border: 1px solid var(--border); - border-radius: 6px; - background: var(--background); + border-radius: 12px; + background: var(--surface); overflow-x: auto; - font-size: 11px; + font-size: 11.5px; + line-height: 1.6; } .agent-connections-notes { color: var(--subtle); - font-size: 10px; + font-size: 10.5px; } +.agent-connections-panel > .secondary-button { + align-self: flex-end; +} + +/* ── 空状态 ── */ + .agent-connections-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 6px; - min-height: 150px; + gap: 10px; + min-height: 180px; + border: 1px dashed var(--border-strong); + border-radius: var(--ac-panel-radius); + background: color-mix(in srgb, var(--surface-alt) 70%, transparent); color: var(--muted); font-size: 12px; - border: 1px dashed var(--border); + line-height: 1.6; + text-align: center; + padding: 24px; +} + +.agent-connections-empty p { + margin: 0; +} + +.agent-connections-empty small { + color: var(--subtle); + font-size: 11px; + max-width: 420px; } @media (max-width: 760px) { + .agent-connections-page { + gap: 16px; + } + .agent-connections-port, .agent-connections-card-copy { flex-direction: column; + align-items: stretch; + } + + .agent-connections-port { + gap: 16px; + } + + .agent-connections-port-controls { + justify-content: flex-start; + max-width: none; + } + + .agent-connections-form { + border-radius: 16px; + padding: 18px; } .agent-connections-form-grid { @@ -426,4 +663,8 @@ .agent-connections-form-wide { grid-column: auto; } + + .agent-connections-card-actions { + justify-content: flex-start; + } } diff --git a/apps/desktop/src/renderer/src/components/pages/AgentStatusPage.tsx b/apps/desktop/src/renderer/src/components/pages/AgentStatusPage.tsx index 8560cad0..6815df01 100644 --- a/apps/desktop/src/renderer/src/components/pages/AgentStatusPage.tsx +++ b/apps/desktop/src/renderer/src/components/pages/AgentStatusPage.tsx @@ -141,6 +141,20 @@ function orbPoint(t: number) { return { x: 50 + t * 1340, y: 210 - Math.sin(t * Math.PI) * 160 } } +const FAILED_RUN_STATUSES = new Set(['failed', 'interrupted', 'timed_out']) + +/** + * 「最近活动」的圆点跟着那一次运行的结果走。 + * 办公室的实时状态一次运行结束就回海(恢复初始状态),失败的痕迹只留在这条记录里, + * 所以这里不能再读 agent.state(那会把刚失败的记录显示成绿灯)。 + */ +function runMarkState(agent: AgentWorkspaceStatus): AgentWorkspaceState { + const status = agent.lastRun?.status + if (!status) return 'idle' + if (FAILED_RUN_STATUSES.has(status)) return 'error' + return status === 'accepted' || status === 'running' ? 'running' : 'idle' +} + function StateMark({ state }: { state: AgentWorkspaceState }) { if (state === 'running') return