feat: workspace layout with sidebar and resize handles

Add a track-based workspace framework around the original workbench panel:

- Main preview area + right sidebar with action icons (focus, disassembly,

  reset, generate-note, open-index)

- Draggable resize handles for sidebar width and bottom panel height

- Keep the original bottom workbench panel design: overview on the left,

  knowledge actions and registered part matches on the right

- Mobile responsive: sidebar becomes a horizontal scrollable row

This keeps the familiar two-column workbench panel while adding

a proper resizable workspace frame so the preview area is no longer

squeezed by the panel.
This commit is contained in:
flash555588 2026-06-06 21:37:50 +08:00
parent 3a57da77e3
commit 355c2e1eb4
7 changed files with 457 additions and 579 deletions

View file

@ -7,12 +7,6 @@
- Preserve STEP XDE component labels during CAD conversion, exporting PCB/CAD components as individual GLB component meshes with `extras.ai3d` identity metadata.
- Remove the direct-workbench explode controls and reorganize the panel around model status, knowledge actions, and registered part matches.
- Preserve source extensions such as STEP, FBX, 3MF, and DAE in analysis records so registered part matching can link reused components across converted model formats.
- Replace the legacy bottom workbench panel with a track-based workspace layout: main preview area gets full remaining space without being squeezed by the panel.
- Add a right sidebar with icon buttons for focus, disassembly, reset, annotation, generate-note, and open-index actions.
- Add draggable resize handles for sidebar width and bottom panel height (like IDE split panes).
- Organize the bottom information panel into Overview / Matches / Knowledge tabs to reduce visual clutter.
- Add tab translation keys for Overview, Matches, and Knowledge in both English and Chinese.
- Make the workspace layout responsive: sidebar becomes a horizontal scrollable row on narrow screens.
## 0.4.3

414
main.js

File diff suppressed because one or more lines are too long

View file

@ -1,36 +0,0 @@
{
"asset": { "version": "2.0", "generator": "test" },
"scene": 0,
"scenes": [{ "nodes": [0] }],
"nodes": [{ "mesh": 0 }],
"meshes": [{
"primitives": [{
"attributes": { "POSITION": 0 },
"indices": 1
}]
}],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 3,
"type": "VEC3",
"max": [1, 1, 0],
"min": [0, 0, 0]
},
{
"bufferView": 1,
"componentType": 5123,
"count": 3,
"type": "SCALAR"
}
],
"bufferViews": [
{ "buffer": 0, "byteOffset": 0, "byteLength": 36 },
{ "buffer": 0, "byteOffset": 36, "byteLength": 6 }
],
"buffers": [{
"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAICPQAAAAAAAAAAAAICPQAAAAAAAAAAAAAAAAAAAAAAPAAABAAIAAw==",
"byteLength": 42
}]
}

View file

@ -8,68 +8,68 @@ export const en = {
"settings.converterMenu.desc": "Closed by default. Open this section only when you want to manually enable an optional local conversion route.",
"settings.environmentInspector": "Environment inspector",
"settings.environmentInspector.desc": "Closed by default. Open this section when you need to configure command paths or run environment checks.",
"settings.paths": "Converter paths",
"settings.diagnostics": "Converter command diagnostics",
"settings.performance": "Performance & display",
"settings.mobileSupport": "Mobile support",
"settings.knowledgeGeneration": "Knowledge generation",
"settings.paths": "Converter paths",
"settings.diagnostics": "Converter command diagnostics",
"settings.performance": "Performance & display",
"settings.mobileSupport": "Mobile support",
"settings.knowledgeGeneration": "Knowledge generation",
// Folders
"settings.sourceModelFolder": "Source model folder",
"settings.sourceModelFolder.desc": "Vault folder where source model files are stored.",
"settings.reportFolder": "Report folder",
"settings.reportFolder.desc": "Vault folder where generated knowledge notes are saved.",
"settings.partFolder": "Part notes folder",
"settings.partFolder.desc": "Vault folder where generated part note drafts are saved.",
"settings.snapshotFolder": "Snapshot folder",
"settings.reportFolder": "Report folder",
"settings.reportFolder.desc": "Vault folder where generated knowledge notes are saved.",
"settings.partFolder": "Part notes folder",
"settings.partFolder.desc": "Vault folder where generated part note drafts are saved.",
"settings.snapshotFolder": "Snapshot folder",
"settings.snapshotFolder.desc": "Vault folder where exported snapshots are saved.",
// Behavior
"settings.autoGenerateKnowledgeNotes": "Auto-generate knowledge notes",
"settings.autoGenerateKnowledgeNotes.desc": "Reserved for a future automation flow. For now, generate notes manually from the command palette.",
"settings.annotationPreviewMode": "Annotation preview mode",
"settings.annotationPreviewMode.desc": "Choose how bound note previews render in annotation popovers and the editor.",
"settings.annotationPreviewMode.plainText": "Plain text",
"settings.annotationPreviewMode.markdown": "Markdown",
"settings.annotationDisplayMode": "Annotation display mode",
"settings.annotationDisplayMode.desc": "Choose how bookmark pins appear over the model.",
"settings.annotationDisplayMode.snippet": "Full snippet",
"settings.annotationDisplayMode.surface": "Single surface",
"settings.annotationDisplayMode.dot": "Single dot",
"settings.previewRendererRollout": "Preview compatibility mode",
"settings.annotationPreviewMode.desc": "Choose how bound note previews render in annotation popovers and the editor.",
"settings.annotationPreviewMode.plainText": "Plain text",
"settings.annotationPreviewMode.markdown": "Markdown",
"settings.annotationDisplayMode": "Annotation display mode",
"settings.annotationDisplayMode.desc": "Choose how bookmark pins appear over the model.",
"settings.annotationDisplayMode.snippet": "Full snippet",
"settings.annotationDisplayMode.surface": "Single surface",
"settings.annotationDisplayMode.dot": "Single dot",
"settings.previewRendererRollout": "Preview compatibility mode",
"settings.previewRendererRollout.desc": "Controls how widely the Three.js preview path is used for single-model previews (GLB, GLTF, STL, PLY, OBJ). Switch back to Compatibility mode if pin projection, occlusion, edit pins, snapshots, or toolbar behavior regress. 3dgrid is not affected by this setting.",
"settings.previewRendererRollout.babylonSafe": "Compatibility mode",
"settings.previewRendererRollout.readonly": "Reading surfaces only",
"settings.previewRendererRollout.direct": "Reading + file view (Recommended)",
"settings.useThreeRenderer": "Use Three.js renderer",
"settings.useThreeRenderer.desc": "Toggle between Three.js (faster, lighter) and Babylon.js (full compatibility) for single-model previews. Three.js supports GLB/GLTF/STL/PLY/OBJ. 3dgrid always uses Babylon.js.",
"settings.experimentalThreeWorkbench": "Experimental Three workbench",
"settings.experimentalThreeWorkbench.desc": "Try the Three.js workbench path for direct GLB/GLTF files only. If loading fails, the file view falls back to Babylon.js automatically.",
"settings.autoRotateDefault": "Auto-rotate by default",
"settings.useThreeRenderer": "Use Three.js renderer",
"settings.useThreeRenderer.desc": "Toggle between Three.js (faster, lighter) and Babylon.js (full compatibility) for single-model previews. Three.js supports GLB/GLTF/STL/PLY/OBJ. 3dgrid always uses Babylon.js.",
"settings.experimentalThreeWorkbench": "Experimental Three workbench",
"settings.experimentalThreeWorkbench.desc": "Try the Three.js workbench path for direct GLB/GLTF files only. If loading fails, the file view falls back to Babylon.js automatically.",
"settings.autoRotateDefault": "Auto-rotate by default",
"settings.autoRotateDefault.desc": "Start model previews with auto-rotation enabled.",
"settings.snapshotNaming": "Snapshot naming",
"settings.snapshotNaming.desc": "How exported snapshot files are named.",
"settings.snapshotNaming.modelName": "Model name + timestamp",
"settings.snapshotNaming.timestamp": "Timestamp only",
"settings.logLevel": "Log level",
"settings.logLevel.desc": "Controls plugin runtime log verbosity in the developer console.",
"settings.language": "Language",
"settings.language.desc": "Display language for plugin settings. Takes effect immediately.",
"settings.analysisMode": "AI drafting mode",
"settings.analysisMode.desc": "Controls whether knowledge-note generation stays local or also requests a sanitized remote draft. Raw model upload is blocked by the current client.",
"settings.analysisMode.local": "Local evidence only",
"settings.analysisMode.hybrid": "Local evidence + remote draft",
"settings.analysisMode.remote": "Remote draft from evidence",
"settings.serviceBaseUrl": "Draft service URL",
"settings.serviceBaseUrl.desc": "Optional base URL for a service that accepts POST /draft-note. Leave empty to keep all drafting local.",
"settings.sendGeometrySummaryToRemote": "Send geometry summary",
"settings.sendGeometrySummaryToRemote.desc": "Allow sanitized mesh counts, bounds, part candidates, annotation coordinates, and nearest-part links to be included in remote draft input.",
"settings.sendPreviewImagesToRemote": "Send preview image references",
"settings.sendPreviewImagesToRemote.desc": "Allow generated preview image paths to be listed in the remote draft input. Image bytes are not uploaded by this client.",
"settings.sendRawModelToRemote": "Send raw model file",
"settings.sendRawModelToRemote.desc": "Reserved for future explicit upload support. If enabled manually, remote draft requests are blocked instead of uploading the model.",
// Converters
"settings.logLevel": "Log level",
"settings.logLevel.desc": "Controls plugin runtime log verbosity in the developer console.",
"settings.language": "Language",
"settings.language.desc": "Display language for plugin settings. Takes effect immediately.",
"settings.analysisMode": "AI drafting mode",
"settings.analysisMode.desc": "Controls whether knowledge-note generation stays local or also requests a sanitized remote draft. Raw model upload is blocked by the current client.",
"settings.analysisMode.local": "Local evidence only",
"settings.analysisMode.hybrid": "Local evidence + remote draft",
"settings.analysisMode.remote": "Remote draft from evidence",
"settings.serviceBaseUrl": "Draft service URL",
"settings.serviceBaseUrl.desc": "Optional base URL for a service that accepts POST /draft-note. Leave empty to keep all drafting local.",
"settings.sendGeometrySummaryToRemote": "Send geometry summary",
"settings.sendGeometrySummaryToRemote.desc": "Allow sanitized mesh counts, bounds, part candidates, annotation coordinates, and nearest-part links to be included in remote draft input.",
"settings.sendPreviewImagesToRemote": "Send preview image references",
"settings.sendPreviewImagesToRemote.desc": "Allow generated preview image paths to be listed in the remote draft input. Image bytes are not uploaded by this client.",
"settings.sendRawModelToRemote": "Send raw model file",
"settings.sendRawModelToRemote.desc": "Reserved for future explicit upload support. If enabled manually, remote draft requests are blocked instead of uploading the model.",
// Converters
"settings.enableCad": "Enable cad conversion for step, iges, and brep files",
"settings.enableCad.desc": "Enable cad conversion for step, iges, and brep formats. Requires cadquery and trimesh in your python environment.",
"settings.enableObj2gltf": "Enable obj2gltf converter (experimental)",
@ -216,8 +216,8 @@ export const en = {
"workbench.previewViewsTitle": "Preview views",
"workbench.connectionsTitle": "Knowledge connections",
"workbench.currentModelLabel": "Current model",
"workbench.noReportYet": "No report note yet",
"workbench.noIndexYet": "No knowledge index yet. Generate a knowledge note first.",
"workbench.noReportYet": "No report note yet",
"workbench.noIndexYet": "No knowledge index yet. Generate a knowledge note first.",
"workbench.noModelLoaded": "No model loaded",
"workbench.disassemblyTitle": "Disassembly",
"workbench.explodeLabel": "Explode",
@ -252,14 +252,14 @@ export const en = {
"workbench.pauseAction": "Pause",
"workbench.saveProfileAction": "Save profile",
"workbench.generateNoteAction": "Generate note",
"workbench.openNoteAction": "Open note",
"workbench.openIndexAction": "Open index",
"workbench.openNoteAction": "Open note",
"workbench.openIndexAction": "Open index",
"workbench.recordTitle": "Specimen card",
"workbench.recordSubtitle": "Current asset profile",
"workbench.notesTitle": "Analysis notes",
"workbench.whereTitle": "Where it connects",
"workbench.noteReady": "Linked note ready",
"workbench.indexReady": "Knowledge index ready",
"workbench.noteReady": "Linked note ready",
"workbench.indexReady": "Knowledge index ready",
"workbench.notePending": "Linked note pending",
"workbench.analysisLabel": "Analysis",
"workbench.settingsUnavailable": "Open this plugin from Obsidian settings to change workbench options.",
@ -269,44 +269,41 @@ export const en = {
"workbench.infoCopied": "Model info copied",
"workbench.templateInserted": "Template inserted",
"workbench.templateCopied": "Template copied",
"workbench.mobileHint": "Mobile tip: tap Interact to move the model, then switch back to Scroll to keep reading the note.",
"workbench.mobileHintInteractive": "Interaction mode is on. Switch back to Scroll when you want to move through the note again.",
"directView.mobileHint": "Mobile tip: use the toolbar Interact button to switch between model interaction and note scrolling.",
"directWorkbench.backendLabel": "Backend",
"directWorkbench.routeLabel": "Route",
"directWorkbench.performanceLabel": "Performance",
"directWorkbench.partCandidatesLabel": "Part candidates",
"directWorkbench.knowledgeTitle": "Knowledge",
"directWorkbench.tabOverview": "Overview",
"directWorkbench.tabMatches": "Matches",
"directWorkbench.tabKnowledge": "Knowledge",
"directWorkbench.registeredTitle": "Registered part matches",
"directWorkbench.registeredLoading": "Checking...",
"directWorkbench.registeredCount": "{count} match(es)",
"directWorkbench.registeredEmpty": "No cross-model part match yet",
"directWorkbench.registeredUnavailable": "Part evidence unavailable",
"directWorkbench.registeredOpen": "Open",
"directWorkbench.registeredOpenNote": "Note",
"directWorkbench.registeredOpenModel": "Model",
"directWorkbench.registeredSourceModel": "From {model}",
"directWorkbench.registeredTargetPartNote": "Opens matched part note",
"directWorkbench.registeredTargetSourceModel": "Opens source model",
"directWorkbench.registeredTargetUnavailable": "No linked target",
"workbench.fileNotFound": "File not found: {path}",
"workbench.mobileHint": "Mobile tip: tap Interact to move the model, then switch back to Scroll to keep reading the note.",
"workbench.mobileHintInteractive": "Interaction mode is on. Switch back to Scroll when you want to move through the note again.",
"directView.mobileHint": "Mobile tip: use the toolbar Interact button to switch between model interaction and note scrolling.",
"directWorkbench.backendLabel": "Backend",
"directWorkbench.routeLabel": "Route",
"directWorkbench.performanceLabel": "Performance",
"directWorkbench.partCandidatesLabel": "Part candidates",
"directWorkbench.knowledgeTitle": "Knowledge",
"directWorkbench.registeredTitle": "Registered part matches",
"directWorkbench.registeredLoading": "Checking...",
"directWorkbench.registeredCount": "{count} match(es)",
"directWorkbench.registeredEmpty": "No cross-model part match yet",
"directWorkbench.registeredUnavailable": "Part evidence unavailable",
"directWorkbench.registeredOpen": "Open",
"directWorkbench.registeredOpenNote": "Note",
"directWorkbench.registeredOpenModel": "Model",
"directWorkbench.registeredSourceModel": "From {model}",
"directWorkbench.registeredTargetPartNote": "Opens matched part note",
"directWorkbench.registeredTargetSourceModel": "Opens source model",
"directWorkbench.registeredTargetUnavailable": "No linked target",
"workbench.fileNotFound": "File not found: {path}",
"annotation.selectColor": "Select color {color}",
"annotation.sectionEmpty": "Section is empty.",
"modal.selectModel": "Select a 3D model...",
"main.commandImportModel": "Import a 3D model",
"main.commandGenerateNote": "Generate knowledge note",
"main.commandOpenKnowledgeIndex": "Open knowledge index",
"main.commandClearConversionCache": "Clear conversion cache",
"main.commandCheckConverters": "Check converters",
"main.commandCopyDiagnostics": "Copy diagnostics report",
"main.converterDiagnosticsMobileUnavailable": "Converter diagnostics are only available on desktop. On iOS, iPadOS, and Android, direct formats can still load.",
"main.diagnosticsCopied": "AI Model Workbench diagnostics copied to clipboard.",
"main.diagnosticsCopyFailed": "Couldn't copy diagnostics. A sanitized diagnostics note was created instead.",
// Inline code block and loading
"main.commandGenerateNote": "Generate knowledge note",
"main.commandOpenKnowledgeIndex": "Open knowledge index",
"main.commandClearConversionCache": "Clear conversion cache",
"main.commandCheckConverters": "Check converters",
"main.commandCopyDiagnostics": "Copy diagnostics report",
"main.converterDiagnosticsMobileUnavailable": "Converter diagnostics are only available on desktop. On iOS, iPadOS, and Android, direct formats can still load.",
"main.diagnosticsCopied": "AI Model Workbench diagnostics copied to clipboard.",
"main.diagnosticsCopyFailed": "Couldn't copy diagnostics. A sanitized diagnostics note was created instead.",
// Inline code block and loading
"codeBlock.noModelPathOrConfig": "No model path or config specified.",
"codeBlock.jsonParseError": "JSON parse error: {error}",
"codeBlock.jsonParseLine": " (line {line})",

View file

@ -10,68 +10,68 @@ export const zhCN: Record<TranslationKey, string> = {
"settings.converterMenu.desc": "默认收起。仅在你需要手动启用某条本地转换路线时再展开。",
"settings.environmentInspector": "环境检查器",
"settings.environmentInspector.desc": "默认收起。仅在需要配置命令路径或手动执行环境检查时再展开。",
"settings.paths": "转换器路径",
"settings.diagnostics": "转换器命令诊断",
"settings.performance": "性能与显示",
"settings.mobileSupport": "移动端支持",
"settings.knowledgeGeneration": "知识生成",
"settings.paths": "转换器路径",
"settings.diagnostics": "转换器命令诊断",
"settings.performance": "性能与显示",
"settings.mobileSupport": "移动端支持",
"settings.knowledgeGeneration": "知识生成",
// 文件夹
"settings.sourceModelFolder": "源模型文件夹",
"settings.sourceModelFolder.desc": "存放源 3D 模型的库文件夹。",
"settings.reportFolder": "报告文件夹",
"settings.reportFolder.desc": "保存生成的知识笔记的库文件夹。",
"settings.partFolder": "部件笔记文件夹",
"settings.partFolder.desc": "保存生成的部件笔记草稿的库文件夹。",
"settings.snapshotFolder": "快照文件夹",
"settings.reportFolder": "报告文件夹",
"settings.reportFolder.desc": "保存生成的知识笔记的库文件夹。",
"settings.partFolder": "部件笔记文件夹",
"settings.partFolder.desc": "保存生成的部件笔记草稿的库文件夹。",
"settings.snapshotFolder": "快照文件夹",
"settings.snapshotFolder.desc": "保存导出快照的库文件夹。",
// 行为
"settings.autoGenerateKnowledgeNotes": "自动生成知识笔记",
"settings.autoGenerateKnowledgeNotes.desc": "预留给后续自动化流程。当前请通过命令或工作台按钮手动生成知识笔记。",
"settings.annotationPreviewMode": "标注预览模式",
"settings.annotationPreviewMode.desc": "选择标注绑定笔记在悬浮预览和编辑器中的显示方式。",
"settings.annotationPreviewMode.plainText": "纯文本",
"settings.annotationPreviewMode.markdown": "Markdown",
"settings.annotationDisplayMode": "标注显示模式",
"settings.annotationDisplayMode.desc": "选择模型上书签标注的显示方式。",
"settings.annotationDisplayMode.snippet": "完整片段",
"settings.annotationDisplayMode.surface": "单个标签面",
"settings.annotationDisplayMode.dot": "单个圆点",
"settings.previewRendererRollout": "预览兼容模式",
"settings.annotationPreviewMode.desc": "选择标注绑定笔记在悬浮预览和编辑器中的显示方式。",
"settings.annotationPreviewMode.plainText": "纯文本",
"settings.annotationPreviewMode.markdown": "Markdown",
"settings.annotationDisplayMode": "标注显示模式",
"settings.annotationDisplayMode.desc": "选择模型上书签标注的显示方式。",
"settings.annotationDisplayMode.snippet": "完整片段",
"settings.annotationDisplayMode.surface": "单个标签面",
"settings.annotationDisplayMode.dot": "单个圆点",
"settings.previewRendererRollout": "预览兼容模式",
"settings.previewRendererRollout.desc": "控制单模型预览GLB、GLTF、STL、PLY、OBJ中 Three.js 渲染路径的启用范围。只要出现 pin 投影、遮挡、编辑态 pin、快照或工具栏行为回退就切回“兼容优先”。workbench 和 3dgrid 不受这个设置影响。",
"settings.previewRendererRollout.babylonSafe": "兼容优先",
"settings.previewRendererRollout.readonly": "仅阅读场景",
"settings.previewRendererRollout.direct": "阅读 + 文件视图(推荐)",
"settings.useThreeRenderer": "使用 Three.js 渲染器",
"settings.useThreeRenderer.desc": "在 Three.js更快、更轻量和 Babylon.js完全兼容之间切换单模型预览渲染器。Three.js 支持 GLB/GLTF/STL/PLY/OBJ。工作台和 3dgrid 始终使用 Babylon.js。",
"settings.experimentalThreeWorkbench": "实验性 Three 工作台",
"settings.experimentalThreeWorkbench.desc": "仅对直读 GLB/GLTF 文件尝试使用 Three.js 工作台路径。加载失败时,文件视图会自动回退到 Babylon.js。",
"settings.autoRotateDefault": "默认自动旋转",
"settings.useThreeRenderer": "使用 Three.js 渲染器",
"settings.useThreeRenderer.desc": "在 Three.js更快、更轻量和 Babylon.js完全兼容之间切换单模型预览渲染器。Three.js 支持 GLB/GLTF/STL/PLY/OBJ。工作台和 3dgrid 始终使用 Babylon.js。",
"settings.experimentalThreeWorkbench": "实验性 Three 工作台",
"settings.experimentalThreeWorkbench.desc": "仅对直读 GLB/GLTF 文件尝试使用 Three.js 工作台路径。加载失败时,文件视图会自动回退到 Babylon.js。",
"settings.autoRotateDefault": "默认自动旋转",
"settings.autoRotateDefault.desc": "启动 3D 预览时默认开启自动旋转。",
"settings.snapshotNaming": "快照命名",
"settings.snapshotNaming.desc": "导出快照文件的命名方式。",
"settings.snapshotNaming.modelName": "模型名 + 时间戳",
"settings.snapshotNaming.timestamp": "仅时间戳",
"settings.logLevel": "日志级别",
"settings.logLevel.desc": "控制插件在开发者控制台中的日志详细程度。",
"settings.language": "语言",
"settings.language.desc": "插件设置界面的显示语言。立即生效。",
"settings.analysisMode": "AI 草稿模式",
"settings.analysisMode.desc": "控制知识笔记生成保持本地,还是额外请求经过裁剪的远程草稿。当前客户端会阻止原始模型上传。",
"settings.analysisMode.local": "仅本地证据",
"settings.analysisMode.hybrid": "本地证据 + 远程草稿",
"settings.analysisMode.remote": "基于证据的远程草稿",
"settings.serviceBaseUrl": "草稿服务 URL",
"settings.serviceBaseUrl.desc": "可选的草稿服务基础地址,服务需接收 POST /draft-note。留空时所有草稿输入都保留在本地。",
"settings.sendGeometrySummaryToRemote": "发送几何摘要",
"settings.sendGeometrySummaryToRemote.desc": "允许把经过裁剪的网格数量、包围盒、部件候选、标注坐标和最近部件关联加入远程草稿输入。",
"settings.sendPreviewImagesToRemote": "发送预览图引用",
"settings.sendPreviewImagesToRemote.desc": "允许在远程草稿输入中列出生成的预览图路径。当前客户端不会上传图片字节。",
"settings.sendRawModelToRemote": "发送原始模型文件",
"settings.sendRawModelToRemote.desc": "预留给未来显式上传支持。若手动开启,远程草稿请求会被阻止,不会上传模型。",
// 转换器
"settings.logLevel": "日志级别",
"settings.logLevel.desc": "控制插件在开发者控制台中的日志详细程度。",
"settings.language": "语言",
"settings.language.desc": "插件设置界面的显示语言。立即生效。",
"settings.analysisMode": "AI 草稿模式",
"settings.analysisMode.desc": "控制知识笔记生成保持本地,还是额外请求经过裁剪的远程草稿。当前客户端会阻止原始模型上传。",
"settings.analysisMode.local": "仅本地证据",
"settings.analysisMode.hybrid": "本地证据 + 远程草稿",
"settings.analysisMode.remote": "基于证据的远程草稿",
"settings.serviceBaseUrl": "草稿服务 URL",
"settings.serviceBaseUrl.desc": "可选的草稿服务基础地址,服务需接收 POST /draft-note。留空时所有草稿输入都保留在本地。",
"settings.sendGeometrySummaryToRemote": "发送几何摘要",
"settings.sendGeometrySummaryToRemote.desc": "允许把经过裁剪的网格数量、包围盒、部件候选、标注坐标和最近部件关联加入远程草稿输入。",
"settings.sendPreviewImagesToRemote": "发送预览图引用",
"settings.sendPreviewImagesToRemote.desc": "允许在远程草稿输入中列出生成的预览图路径。当前客户端不会上传图片字节。",
"settings.sendRawModelToRemote": "发送原始模型文件",
"settings.sendRawModelToRemote.desc": "预留给未来显式上传支持。若手动开启,远程草稿请求会被阻止,不会上传模型。",
// 转换器
"settings.enableCad": "启用 CAD 转换器 (STEP/IGES/BREP)",
"settings.enableCad.desc": "启用 STEP/IGES/BREP 格式的 CAD 转换路线,通过 Python CadQuery (OpenCASCADE)。需要pip install cadquery trimesh",
"settings.enableObj2gltf": "启用 obj2gltf 转换器(实验性)",
@ -218,8 +218,8 @@ export const zhCN: Record<TranslationKey, string> = {
"workbench.previewViewsTitle": "预览视图",
"workbench.connectionsTitle": "知识连接",
"workbench.currentModelLabel": "当前模型",
"workbench.noReportYet": "还没有报告笔记",
"workbench.noIndexYet": "还没有知识索引。请先生成知识笔记。",
"workbench.noReportYet": "还没有报告笔记",
"workbench.noIndexYet": "还没有知识索引。请先生成知识笔记。",
"workbench.noModelLoaded": "尚未加载模型",
"workbench.disassemblyTitle": "分解",
"workbench.explodeLabel": "爆炸",
@ -254,14 +254,14 @@ export const zhCN: Record<TranslationKey, string> = {
"workbench.pauseAction": "暂停",
"workbench.saveProfileAction": "保存配置",
"workbench.generateNoteAction": "生成笔记",
"workbench.openNoteAction": "打开笔记",
"workbench.openIndexAction": "打开索引",
"workbench.openNoteAction": "打开笔记",
"workbench.openIndexAction": "打开索引",
"workbench.recordTitle": "标本卡",
"workbench.recordSubtitle": "当前资产档案",
"workbench.notesTitle": "分析札记",
"workbench.whereTitle": "关联位置",
"workbench.noteReady": "关联笔记已就绪",
"workbench.indexReady": "知识索引已就绪",
"workbench.noteReady": "关联笔记已就绪",
"workbench.indexReady": "知识索引已就绪",
"workbench.notePending": "关联笔记待生成",
"workbench.analysisLabel": "分析",
"workbench.settingsUnavailable": "请从 Obsidian 设置中打开本插件来调整工作台选项。",
@ -271,44 +271,41 @@ export const zhCN: Record<TranslationKey, string> = {
"workbench.infoCopied": "模型信息已复制",
"workbench.templateInserted": "模板已插入",
"workbench.templateCopied": "模板已复制",
"workbench.mobileHint": "移动端提示:需要操作模型时点“交互”,继续阅读笔记时切回“滚动”。",
"workbench.mobileHintInteractive": "当前为交互模式。需要继续滚动笔记时,请切回“滚动”。",
"directView.mobileHint": "移动端提示:使用下方工具栏里的“交互”按钮,在模型操作和笔记滚动之间切换。",
"directWorkbench.backendLabel": "后端",
"directWorkbench.routeLabel": "路由",
"directWorkbench.performanceLabel": "性能",
"directWorkbench.partCandidatesLabel": "候选零件",
"directWorkbench.knowledgeTitle": "知识",
"directWorkbench.tabOverview": "概览",
"directWorkbench.tabMatches": "匹配",
"directWorkbench.tabKnowledge": "知识",
"directWorkbench.registeredTitle": "已注册零件匹配",
"directWorkbench.registeredLoading": "检查中...",
"directWorkbench.registeredCount": "{count} 个匹配",
"directWorkbench.registeredEmpty": "暂无跨模型零件匹配",
"directWorkbench.registeredUnavailable": "暂无零件证据",
"directWorkbench.registeredOpen": "打开",
"directWorkbench.registeredOpenNote": "笔记",
"directWorkbench.registeredOpenModel": "模型",
"directWorkbench.registeredSourceModel": "来自 {model}",
"directWorkbench.registeredTargetPartNote": "打开匹配零件笔记",
"directWorkbench.registeredTargetSourceModel": "打开来源模型",
"directWorkbench.registeredTargetUnavailable": "暂无可打开目标",
"workbench.fileNotFound": "未找到文件:{path}",
"workbench.mobileHint": "移动端提示:需要操作模型时点“交互”,继续阅读笔记时切回“滚动”。",
"workbench.mobileHintInteractive": "当前为交互模式。需要继续滚动笔记时,请切回“滚动”。",
"directView.mobileHint": "移动端提示:使用下方工具栏里的“交互”按钮,在模型操作和笔记滚动之间切换。",
"directWorkbench.backendLabel": "后端",
"directWorkbench.routeLabel": "路由",
"directWorkbench.performanceLabel": "性能",
"directWorkbench.partCandidatesLabel": "候选零件",
"directWorkbench.knowledgeTitle": "知识",
"directWorkbench.registeredTitle": "已注册零件匹配",
"directWorkbench.registeredLoading": "检查中...",
"directWorkbench.registeredCount": "{count} 个匹配",
"directWorkbench.registeredEmpty": "暂无跨模型零件匹配",
"directWorkbench.registeredUnavailable": "暂无零件证据",
"directWorkbench.registeredOpen": "打开",
"directWorkbench.registeredOpenNote": "笔记",
"directWorkbench.registeredOpenModel": "模型",
"directWorkbench.registeredSourceModel": "来自 {model}",
"directWorkbench.registeredTargetPartNote": "打开匹配零件笔记",
"directWorkbench.registeredTargetSourceModel": "打开来源模型",
"directWorkbench.registeredTargetUnavailable": "暂无可打开目标",
"workbench.fileNotFound": "未找到文件:{path}",
"annotation.selectColor": "选择颜色 {color}",
"annotation.sectionEmpty": "该段内容为空。",
"modal.selectModel": "选择一个 3D 模型...",
"main.commandImportModel": "导入 3D 模型",
"main.commandGenerateNote": "生成知识笔记",
"main.commandOpenKnowledgeIndex": "打开知识索引",
"main.commandClearConversionCache": "清除转换缓存",
"main.commandCheckConverters": "检查转换器",
"main.commandCopyDiagnostics": "复制诊断报告",
"main.converterDiagnosticsMobileUnavailable": "转换器诊断仅支持桌面端。在 iOS、iPadOS 和 Android 上,直读格式仍可加载。",
"main.diagnosticsCopied": "AI Model Workbench 诊断报告已复制到剪贴板。",
"main.diagnosticsCopyFailed": "无法复制诊断报告,已改为创建一份脱敏诊断笔记。",
// 内联代码块与加载
"main.commandGenerateNote": "生成知识笔记",
"main.commandOpenKnowledgeIndex": "打开知识索引",
"main.commandClearConversionCache": "清除转换缓存",
"main.commandCheckConverters": "检查转换器",
"main.commandCopyDiagnostics": "复制诊断报告",
"main.converterDiagnosticsMobileUnavailable": "转换器诊断仅支持桌面端。在 iOS、iPadOS 和 Android 上,直读格式仍可加载。",
"main.diagnosticsCopied": "AI Model Workbench 诊断报告已复制到剪贴板。",
"main.diagnosticsCopyFailed": "无法复制诊断报告,已改为创建一份脱敏诊断笔记。",
// 内联代码块与加载
"codeBlock.noModelPathOrConfig": "未指定模型路径或配置。",
"codeBlock.jsonParseError": "JSON 解析错误:{error}",
"codeBlock.jsonParseLine": "(第 {line} 行)",

View file

@ -3,7 +3,6 @@ import type { PluginSettings, ModelAssetProfile, ModelPreviewSummary, ModelEvide
import { AnnotationManager } from "../render/preview/annotations";
import { createLoggedModelPreview } from "../render/preview/selection";
import type { AnnotationPreview } from "../render/preview/types";
import { supportsDisassemblyPreview, supportsFocusSelectionPreview } from "../render/preview/types";
import { createHelperButtons } from "./inline/helper-buttons";
import { createConversionManager } from "../io/conversion/factory";
import type { ConvertedAssetCache } from "../io/cache/converted-asset-cache";
@ -101,9 +100,6 @@ export class DirectModelView extends FileView {
private workbenchSummary: ModelPreviewSummary | null = null;
private workbenchRoute: { backend: string; reason: string } | null = null;
private workbenchModelPath: string | null = null;
private activeTab: "overview" | "matches" | "knowledge" = "overview";
private bottomPanel: HTMLElement | null = null;
private tabButtons: Map<string, HTMLButtonElement> = new Map();
constructor(leaf: WorkspaceLeaf, getSettings: () => PluginSettings, convertedAssetCache: ConvertedAssetCache, ps: PluginStore) {
super(leaf);
@ -160,8 +156,6 @@ export class DirectModelView extends FileView {
this.workbenchSummary = null;
this.workbenchRoute = null;
this.workbenchModelPath = null;
this.bottomPanel = null;
this.tabButtons.clear();
this.preview?.destroy();
this.preview = null;
this.ps.store.setState({
@ -169,7 +163,7 @@ export class DirectModelView extends FileView {
modelPreview: null,
selectedPart: null,
});
// === New workspace layout ===
// === Workspace layout with draggable resize ===
const workspace = this.contentEl.createDiv({ cls: "ai3d-workspace" });
// Top track: main preview + sidebar
const topTrack = workspace.createDiv({ cls: "ai3d-workspace-track-top" });
@ -229,10 +223,8 @@ export class DirectModelView extends FileView {
this.createSidebarButtons(sidebar, file);
// Vertical resize handle + bottom panel
const vHandle = workspace.createDiv({ cls: "ai3d-resize-handle ai3d-resize-handle-v" });
const bottomPanel = workspace.createDiv({ cls: "ai3d-workspace-bottom is-hidden" });
this.bottomPanel = bottomPanel;
// Setup tabs and resize handles
this.workbenchPanel = this.createBottomTabs(bottomPanel, file);
const workbenchPanel = workspace.createDiv({ cls: "ai3d-direct-workbench-panel is-hidden" });
this.workbenchPanel = workbenchPanel;
this.setupResizeHandles(hHandle, vHandle, topTrack, workspace);
if (mobile) {
mainArea.createDiv({
@ -278,11 +270,11 @@ export class DirectModelView extends FileView {
const evidence = this.preview.getModelEvidence?.() ?? null;
this.registerModelPartsFromEvidence(file.path, evidence);
renderModelPerformanceFeedback(host, summary);
this.workbenchPanel = this.workbenchPanel;
this.workbenchPanel = workbenchPanel;
this.workbenchSummary = summary;
this.workbenchRoute = created.route;
this.workbenchModelPath = file.path;
this.renderWorkbenchPanel(this.workbenchPanel, summary, created.route, file.path);
this.renderWorkbenchPanel(workbenchPanel, summary, created.route, file.path);
this.ps.store.setState({
currentModelPath: file.path,
modelPreview: summary,
@ -353,7 +345,7 @@ export class DirectModelView extends FileView {
this.preview?.destroy();
this.preview = null;
host.replaceChildren();
this.bottomPanel?.addClass("is-hidden");
this.workbenchPanel?.addClass("is-hidden");
const failure = describeModelLoadFailure(err);
if (isMissingConverterError(err)) {
console.warn("[AI3D] Direct view blocked by converter settings:", failure.message);
@ -406,24 +398,7 @@ export class DirectModelView extends FileView {
panel.removeClass("is-hidden");
panel.dataset.ai3dBackend = route.backend;
panel.dataset.ai3dRouteReason = route.reason;
switch (this.activeTab) {
case "overview":
this.renderOverviewTab(panel, summary, route, modelPath);
break;
case "matches":
this.renderMatchesTab(panel, modelPath, summary);
break;
case "knowledge":
this.renderKnowledgeTab(panel, modelPath);
break;
}
}
private renderOverviewTab(
panel: HTMLElement,
summary: ModelPreviewSummary,
route: { backend: string; reason: string },
modelPath: string,
): void {
const overview = panel.createDiv({ cls: "ai3d-direct-workbench-overview" });
const status = overview.createDiv({ cls: "ai3d-direct-workbench-status" });
const backendLine = status.createDiv({ cls: "ai3d-direct-workbench-line" });
@ -432,6 +407,7 @@ export class DirectModelView extends FileView {
const routeLine = status.createDiv({ cls: "ai3d-direct-workbench-line ai3d-direct-workbench-route" });
routeLine.createSpan({ cls: "ai3d-direct-workbench-label", text: t("directWorkbench.routeLabel") });
routeLine.createSpan({ cls: "ai3d-direct-workbench-value", text: route.reason });
const metrics = overview.createDiv({ cls: "ai3d-direct-workbench-metrics" });
this.renderMetric(metrics, t("workbench.meshesLabel"), formatCount(summary.meshCount));
this.renderMetric(
@ -447,19 +423,17 @@ export class DirectModelView extends FileView {
this.renderMetric(metrics, t("workbench.materialsLabel"), formatCount(summary.materialCount));
this.renderMetric(metrics, t("workbench.boundingSizeLabel"), formatBounds(summary));
this.renderMetric(metrics, t("directWorkbench.performanceLabel"), summary.performanceTier ?? "light");
const controls = panel.createDiv({ cls: "ai3d-direct-workbench-controls" });
this.renderKnowledgeControls(controls, modelPath);
this.renderRegisteredPartMatches(controls, modelPath, summary);
}
private renderMatchesTab(
panel: HTMLElement,
modelPath: string,
summary: ModelPreviewSummary,
): void {
this.renderRegisteredPartMatches(panel, modelPath, summary);
}
private renderKnowledgeTab(
panel: HTMLElement,
modelPath: string,
): void {
this.renderKnowledgeControls(panel, modelPath);
private refreshWorkbenchPanel(): void {
if (!this.workbenchPanel || !this.workbenchSummary || !this.workbenchRoute || !this.workbenchModelPath) {
return;
}
this.renderWorkbenchPanel(this.workbenchPanel, this.workbenchSummary, this.workbenchRoute, this.workbenchModelPath);
}
private createSidebarButtons(sidebar: HTMLElement, file: TFile): void {
const createBtn = (action: string, label: string, icon: string, onClick: () => void): void => {
@ -471,24 +445,26 @@ export class DirectModelView extends FileView {
btn.addEventListener("click", onClick);
};
createBtn("toggle-focus", t("helper.toggleFocusSelectionLabel"), `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>`, () => {
if (this.preview && supportsFocusSelectionPreview(this.preview)) {
this.preview.toggleFocusSelection();
if (this.preview) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = this.preview as any;
if (typeof p.toggleFocusSelection === "function") p.toggleFocusSelection();
}
});
createBtn("toggle-disassembly", t("helper.toggleDisassemblyLabel"), `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7V4h3"/><path d="M4 17v3h3"/><path d="M20 7V4h-3"/><path d="M20 17v3h-3"/></svg>`, () => {
if (this.preview && supportsDisassemblyPreview(this.preview)) {
this.preview.toggleDisassembly();
if (this.preview) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = this.preview as any;
if (typeof p.toggleDisassembly === "function") p.toggleDisassembly();
}
});
createBtn("reset-parts", t("helper.resetPartsLabel"), `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74-2.74L3 12"/></svg>`, () => {
if (this.preview && supportsDisassemblyPreview(this.preview)) {
this.preview.resetDisassembly();
if (this.preview) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = this.preview as any;
if (typeof p.resetDisassembly === "function") p.resetDisassembly();
}
});
createBtn("toggle-annotation", t("helper.toggleAnnotationLabel"), `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h6"/></svg>`, () => {
// annotation toggle is handled by helper toolbar, sync sidebar state
// This is a simplified version; ideally we sync with the main toolbar
});
createBtn("generate-note", t("workbench.generateNoteAction"), `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`, () => {
const btn = sidebar.querySelector('[data-ai3d-action="generate-note"]');
if (btn instanceof HTMLButtonElement) btn.disabled = true;
@ -511,35 +487,6 @@ export class DirectModelView extends FileView {
}
});
}
private createBottomTabs(bottomPanel: HTMLElement, file: TFile): HTMLElement {
const tabHeader = bottomPanel.createDiv({ cls: "ai3d-workspace-bottom-tabs" });
const tabContent = bottomPanel.createDiv({ cls: "ai3d-workspace-bottom-content" });
const tabs: { id: "overview" | "matches" | "knowledge"; labelKey: Parameters<typeof t>[0] }[] = [
{ id: "overview", labelKey: "directWorkbench.tabOverview" },
{ id: "matches", labelKey: "directWorkbench.tabMatches" },
{ id: "knowledge", labelKey: "directWorkbench.tabKnowledge" },
];
for (const tab of tabs) {
const btn = tabHeader.createEl("button", {
cls: "ai3d-workspace-tab-btn",
text: t(tab.labelKey),
attr: { type: "button", "data-tab": tab.id },
});
btn.addEventListener("click", () => {
this.activeTab = tab.id;
this.refreshWorkbenchPanel();
this.updateTabButtons();
});
this.tabButtons.set(tab.id, btn);
}
this.updateTabButtons();
return tabContent;
}
private updateTabButtons(): void {
for (const [id, btn] of this.tabButtons) {
btn.classList.toggle("is-active", id === this.activeTab);
}
}
private setupResizeHandles(
hHandle: HTMLElement,
vHandle: HTMLElement,
@ -584,14 +531,6 @@ export class DirectModelView extends FileView {
workspace.style.gridTemplateRows = `1fr 4px ${bottomHeight}px`;
}, () => {});
}
private refreshWorkbenchPanel(): void {
if (!this.workbenchPanel || !this.workbenchSummary || !this.workbenchRoute || !this.workbenchModelPath) {
return;
}
this.renderWorkbenchPanel(this.workbenchPanel, this.workbenchSummary, this.workbenchRoute, this.workbenchModelPath);
}
private renderMetric(parent: HTMLElement, label: string, value: string): void {
const metric = parent.createDiv({ cls: "ai3d-direct-workbench-metric" });
metric.createSpan({ cls: "ai3d-direct-workbench-label", text: label });

View file

@ -502,7 +502,6 @@ body {
height: 100%;
min-height: 0;
}
/* Top track: main + sidebar */
.ai3d-workspace-track-top {
display: grid;
grid-template-columns: 1fr 4px 200px;
@ -523,7 +522,6 @@ body {
.ai3d-workspace-main .ai3d-helper-toolbar {
flex-shrink: 0;
}
/* Sidebar */
.ai3d-workspace-sidebar {
display: flex;
flex-direction: column;
@ -559,7 +557,6 @@ body {
opacity: 0.4;
cursor: not-allowed;
}
/* Resize handles */
.ai3d-resize-handle {
background: var(--background-modifier-border);
user-select: none;
@ -576,54 +573,11 @@ body {
.ai3d-resize-handle-v {
cursor: row-resize;
}
/* Bottom panel */
.ai3d-workspace-bottom {
display: flex;
flex-direction: column;
background: var(--background-secondary);
border-top: 1px solid var(--background-modifier-border);
min-height: 0;
overflow: hidden;
}
.ai3d-workspace-bottom.is-hidden {
display: none;
}
.ai3d-workspace-bottom-tabs {
display: flex;
gap: 2px;
padding: 4px 6px 0;
border-bottom: 1px solid var(--background-modifier-border);
flex-shrink: 0;
background: var(--background-secondary);
}
.ai3d-workspace-tab-btn {
padding: 4px 12px;
border: 1px solid transparent;
border-bottom: none;
border-radius: var(--radius-s) var(--radius-s) 0 0;
background: transparent;
color: var(--text-muted);
font-size: var(--font-ui-smaller);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.ai3d-workspace-tab-btn:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.ai3d-workspace-tab-btn.is-active {
background: var(--background-primary);
color: var(--text-normal);
border-color: var(--background-modifier-border);
font-weight: 600;
}
.ai3d-workspace-bottom-content {
.ai3d-direct-view .ai3d-preview-host {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px;
}
/* Legacy workbench panel styles (used inside tabs) */
.ai3d-direct-workbench-panel {
display: grid;
grid-template-columns: minmax(220px, 0.9fr) minmax(320px, 1.4fr);
@ -636,6 +590,7 @@ body {
background: var(--background-secondary);
font-size: var(--font-ui-smaller);
}
.ai3d-direct-workbench-overview,
.ai3d-direct-workbench-status,
.ai3d-direct-workbench-metrics,
@ -644,15 +599,18 @@ body {
display: flex;
gap: 8px;
}
.ai3d-direct-workbench-overview,
.ai3d-direct-workbench-status,
.ai3d-direct-workbench-controls {
flex-direction: column;
}
.ai3d-direct-workbench-metrics {
flex-wrap: wrap;
align-content: flex-start;
}
.ai3d-direct-workbench-line,
.ai3d-direct-workbench-metric,
.ai3d-direct-workbench-control {
@ -662,106 +620,128 @@ body {
border-radius: var(--radius-s);
background: var(--background-primary);
}
.ai3d-direct-workbench-line,
.ai3d-direct-workbench-metric {
display: flex;
flex-direction: column;
gap: 2px;
}
.ai3d-direct-workbench-metric {
flex: 1 1 84px;
}
.ai3d-direct-workbench-route {
flex: 1;
}
.ai3d-direct-workbench-label {
color: var(--text-muted);
font-size: 11px;
line-height: 1.25;
}
.ai3d-direct-workbench-value {
min-width: 0;
color: var(--text-normal);
font-weight: 600;
font-size: 12px;
line-height: 1.3;
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai3d-direct-workbench-control {
display: flex;
flex-direction: column;
gap: 6px;
}
.ai3d-direct-workbench-control-head,
.ai3d-direct-workbench-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.ai3d-direct-workbench-control-head {
justify-content: space-between;
}
.ai3d-direct-workbench-action {
padding: 5px 10px;
min-height: 26px;
padding: 2px 8px;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
color: var(--text-normal);
color: var(--text-muted);
font-size: var(--font-ui-smaller);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.ai3d-direct-workbench-action:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
border-color: var(--interactive-accent);
}
.ai3d-direct-workbench-action:disabled {
opacity: 0.45;
cursor: not-allowed;
cursor: default;
opacity: 0.55;
}
.ai3d-direct-workbench-knowledge {
gap: 4px;
}
.ai3d-direct-workbench-registered {
gap: 6px;
}
.ai3d-direct-workbench-registered-body {
min-width: 0;
}
.ai3d-direct-workbench-empty {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
padding: 8px 0;
line-height: 1.35;
}
.ai3d-direct-workbench-match-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.ai3d-direct-workbench-match {
display: grid;
grid-template-columns: 1fr auto;
gap: 6px;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
padding: 6px 8px;
min-width: 0;
padding: 6px;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
background: var(--background-primary);
background: var(--background-secondary);
}
.ai3d-direct-workbench-match-main,
.ai3d-direct-workbench-match-side {
min-width: 0;
}
.ai3d-direct-workbench-match-main {
display: flex;
flex-direction: column;
gap: 2px;
}
.ai3d-direct-workbench-match-side {
display: flex;
gap: 6px;
align-items: center;
gap: 6px;
}
.ai3d-direct-workbench-match-title,
.ai3d-direct-workbench-match-source,
.ai3d-direct-workbench-match-model,
@ -772,34 +752,40 @@ body {
text-overflow: ellipsis;
white-space: nowrap;
}
.ai3d-direct-workbench-match-title {
color: var(--text-normal);
font-weight: 600;
font-size: 12px;
}
.ai3d-direct-workbench-match-source {
color: var(--text-muted);
}
.ai3d-direct-workbench-match-model {
color: var(--text-faint);
}
.ai3d-direct-workbench-match-target {
color: var(--text-muted);
font-size: 11px;
color: var(--text-faint);
font-size: var(--font-ui-smaller);
}
.ai3d-direct-workbench-match-reasons {
color: var(--text-faint);
font-size: 10px;
}
.ai3d-direct-workbench-match-score {
color: var(--text-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
min-width: 34px;
color: var(--text-accent);
font-weight: 700;
text-align: right;
}
.ai3d-direct-workbench-match-open {
min-width: 44px;
}
/* Mobile responsive */
@media (max-width: 760px) {
.ai3d-workspace-track-top {
grid-template-columns: 1fr;
@ -829,6 +815,7 @@ body {
justify-content: space-between;
}
}
/* --- Responsive: Narrow Sidebar --- */
@media (max-width: 400px) {