fix(minimap): 修复页面加载时小地图自动展开的问题

- 添加初始化标志跳过首次 viewport 变化检测,避免页面加载时误触发
- 优化 AIInputBar 提交冷却时间从 3 秒改为 1 秒
- 修复 SW 执行路径提前 return 导致冷却定时器未启动的问题
- 在 postProcessingCompleted 事件中立即重置 isSubmitting 状态
- 注释掉多个调试日志减少控制台噪音
- 更新 CLAUDE.md 添加分页加载与虚拟滚动功能文档
- 更新 CODEBUDDY.md 添加多分支函数共享逻辑的最佳实践

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
liquidliangandClaude committed 2026-01-13 20:33:13 +08:00
1 parent 2d88b988ec
commit 9cd009dfb9
9 files changed
+251 -55

No files matched your search

+6 -6
View File
@@ -1,10 +1,10 @@
# Update CODEBUDDY.md
# Update CLAUDE.md
分析代码审查反馈或开发过程中发现的问题,提取通用规则并更新 CODEBUDDY.md。
分析代码审查反馈或开发过程中发现的问题,提取通用规则并更新 CLAUDE.md。
## 说明
这个命令实现了 Boris Cherny 第 5 条技巧:Code Review 自动更新规则到 CODEBUDDY.md,让 AI 越来越懂你的项目。形成知识沉淀的飞轮效应。
这个命令实现了 Boris Cherny 第 5 条技巧:Code Review 自动更新规则到 CLAUDE.md,让 AI 越来越懂你的项目。形成知识沉淀的飞轮效应。
## 参数
@@ -55,7 +55,7 @@
### 4. 定位更新位置
读取 CODEBUDDY.md,确定规则应该添加到哪个章节:
读取 CLAUDE.md,确定规则应该添加到哪个章节:
- 开发规范相关 → `### Development Rules`
- 编码标准相关 → `### Coding Standards`
@@ -76,7 +76,7 @@
### 6. 更新文件
使用 Edit 工具更新 CODEBUDDY.md:
使用 Edit 工具更新 CLAUDE.md:
- 保持现有格式
- 在适当位置插入新规则
- 如果需要,创建新章节
@@ -85,7 +85,7 @@
显示更新内容摘要:
```markdown
## 已更新 CODEBUDDY.md
## 已更新 CLAUDE.md
### 新增规则
+102 -2
View File
@@ -10,7 +10,7 @@
**项目信息:**
- **名称**: Aitu (爱图) - AI 图片与视频创作工具
- **版本**: 0.4.0
- **版本**: 0.5.0
- **许可证**: MIT
- **标语**: 爱上图片,爱上创作
- **官网**: https://aitu.tu-zi.com
@@ -125,7 +125,7 @@ packages/drawnix/
│ ├── components/ # UI 组件 (43个子目录)
│ ├── services/ # 业务逻辑服务 (32个服务)
│ ├── plugins/ # 功能插件 (15个插件)
│ ├── hooks/ # React Hooks (24个)
│ ├── hooks/ # React Hooks (27个)
│ ├── useWorkflowSubmission.ts # 工作流提交(核心)
│ ├── useAutoInsertToCanvas.ts # 自动插入画布
│ ├── utils/ # 工具函数 (33个模块)
@@ -177,6 +177,9 @@ components/
├── settings-dialog/ # 设置对话框
├── project-drawer/ # 项目抽屉
├── task-queue/ # 任务队列 UI
│ └── VirtualTaskList.tsx # 虚拟任务列表(支持分页和虚拟滚动)
├── lazy-image/ # 懒加载图片组件
│ └── LazyImage.tsx # 基于 IntersectionObserver 的图片懒加载
├── video-frame-selector/ # 视频帧选择器
├── generation-history/ # 生成历史
├── minimap/ # 小地图
@@ -254,6 +257,9 @@ hooks/
├── useMention.ts # @提及功能
├── useMediaCache.ts # 媒体缓存
├── useViewportScale.ts # 视口缩放
├── useInfinitePagination.ts # 无限滚动分页
├── useVirtualList.ts # 虚拟列表(封装 @tanstack/react-virtual)
├── useImageLazyLoad.ts # 图片懒加载
└── ...其他 Hooks
```
@@ -971,6 +977,100 @@ const result = await backupRestoreService.importFromZip(file, onProgress);
- 虚拟 URL(`/asset-library/`)从 Cache API 获取
- 导入时区分本地素材和 AI 生成素材,存储位置不同
### 分页加载与虚拟滚动
支持任务队列和素材库的分页加载与虚拟滚动,优化大数据量场景下的性能。
**核心文件**:
- `hooks/useInfinitePagination.ts` - 无限滚动分页 Hook
- `hooks/useVirtualList.ts` - 虚拟列表 Hook(封装 @tanstack/react-virtual)
- `hooks/useImageLazyLoad.ts` - 图片懒加载 Hook
- `components/lazy-image/LazyImage.tsx` - 懒加载图片组件
- `components/task-queue/VirtualTaskList.tsx` - 虚拟任务列表组件
- `components/media-library/VirtualAssetGrid.tsx` - 虚拟素材网格组件
- `apps/web/src/sw/task-queue/storage.ts` - IndexedDB 游标分页查询
**功能特点**:
- IndexedDB 游标分页:支持大数据量的高效分页查询
- 无限滚动:滚动到底部自动加载更多数据
- 虚拟滚动:只渲染可见区域的元素,大幅减少 DOM 节点
- 图片懒加载:基于 IntersectionObserver,进入视口才加载图片
- 实时更新:支持 `prependItems`、`updateItem`、`removeItem` 操作
**使用示例**:
```typescript
// 无限滚动分页
const {
items,
isLoading,
isLoadingMore,
hasMore,
loadMore,
reset,
} = useInfinitePagination({
fetcher: async ({ offset, limit }) => {
const result = await swTaskQueueClient.requestPaginatedTasks({
offset,
limit,
status: filterStatus,
});
return {
items: result.tasks,
total: result.total,
hasMore: result.hasMore,
};
},
pageSize: 50,
getItemKey: (task) => task.id,
deps: [filterStatus],
});
// 虚拟列表
const { parentRef, virtualItems, totalSize, getItem } = useVirtualList({
items: tasks,
estimateSize: 200,
overscan: 3,
});
```
**IndexedDB 分页查询**:
```typescript
// Service Worker 中的游标分页实现
async getPaginatedTasks(params: PaginationParams): Promise<PaginatedResult> {
const { offset = 0, limit = 50, status } = params;
const db = await this.getDB();
const tx = db.transaction('tasks', 'readonly');
const store = tx.objectStore('tasks');
const index = store.index('by-createdAt');
let cursor = await index.openCursor(null, 'prev');
let skipped = 0;
const items: Task[] = [];
while (cursor && items.length < limit) {
if (status && cursor.value.status !== status) {
cursor = await cursor.continue();
continue;
}
if (skipped < offset) {
skipped++;
cursor = await cursor.continue();
continue;
}
items.push(cursor.value);
cursor = await cursor.continue();
}
return { items, total, hasMore: offset + items.length < total };
}
```
**性能优化**:
- 默认每页 50 条数据
- 虚拟列表 overscan 设置为 3-5 个元素
- 图片懒加载 rootMargin 设置为 200px(提前加载)
- 使用 `getItemKey` 进行去重,避免重复数据
---
## 开发规范
+62
View File
@@ -314,6 +314,68 @@ useEffect(() => {
**原因**: 事件处理中的清理操作(如删除 UI 元素)通常通过 ref 引用,不依赖于事件携带的数据。如果在检查事件数据有效性时过早 `return`,会导致清理逻辑被跳过,造成 UI 残留(如 WorkZone 面板未关闭)。应该将独立的操作分离,确保关键清理逻辑总能执行。
#### 多分支函数中提前 return 导致共享逻辑未执行
**场景**: 异步函数有多个执行路径(如 SW 执行和主线程 fallback),每个分支都需要执行某些共享逻辑(如重置状态、启动定时器)
❌ **错误示例**:
```typescript
const handleGenerate = async () => {
setIsSubmitting(true);
try {
const { usedSW } = await submitWorkflowToSW(params);
if (usedSW) {
// SW 执行成功
setPrompt('');
return; // ❌ 提前返回,跳过了冷却定时器
}
} catch (error) {
// fallback to main thread
}
// 主线程执行...
await executeInMainThread();
// 启动冷却定时器(只有主线程路径能执行到这里)
submitCooldownRef.current = setTimeout(() => {
setIsSubmitting(false); // ❌ SW 路径永远不会重置!
}, 1000);
};
```
✅ **正确示例**:
```typescript
const handleGenerate = async () => {
setIsSubmitting(true);
try {
const { usedSW } = await submitWorkflowToSW(params);
if (usedSW) {
// SW 执行成功
setPrompt('');
// ✅ 在 return 前执行共享逻辑
submitCooldownRef.current = setTimeout(() => {
setIsSubmitting(false);
}, 1000);
return;
}
} catch (error) {
// fallback to main thread
}
// 主线程执行...
await executeInMainThread();
// 主线程路径也执行相同的冷却逻辑
submitCooldownRef.current = setTimeout(() => {
setIsSubmitting(false);
}, 1000);
};
```
**原因**: 当函数有多个执行路径时,每个路径都可能需要执行某些共享逻辑(如重置状态)。如果在某个分支中提前 `return` 而忘记执行共享逻辑,会导致状态不一致(如 `isSubmitting` 永远为 `true`,输入框被禁用)。解决方案:1) 在每个 `return` 前确保执行共享逻辑;2) 或将共享逻辑提取到 `finally` 块中。
#### CSS/SCSS Guidelines
- Use BEM naming convention
- Prefer design system CSS variables (see Brand Guidelines below)
@@ -522,6 +522,14 @@ export const AIInputBar: React.FC<AIInputBarProps> = React.memo(({ className, is
if (event.type === 'postProcessingCompleted') {
const position = event.result.firstElementPosition;
// 立即重置提交状态,允许用户继续输入
// console.log('[AIInputBar] postProcessingCompleted: resetting isSubmitting');
if (submitCooldownRef.current) {
clearTimeout(submitCooldownRef.current);
submitCooldownRef.current = null;
}
setIsSubmitting(false);
// 关闭 ChatDrawer(如果是由 AIInputBar 触发的对话)
// 注意:这里使用 setTimeout 确保消息更新后再关闭
setTimeout(() => {
@@ -829,8 +837,12 @@ export const AIInputBar: React.FC<AIInputBarProps> = React.memo(({ className, is
// Handle generation
const handleGenerate = useCallback(async () => {
if (!prompt.trim() && allContent.length === 0) return;
if (isSubmitting) return; // 仅防止快速重复点击
if (isSubmitting) {
// console.log('[AIInputBar] handleGenerate blocked: isSubmitting=true');
return; // 仅防止快速重复点击
}
// console.log('[AIInputBar] handleGenerate: setting isSubmitting=true');
setIsSubmitting(true);
try {
@@ -1056,12 +1068,22 @@ export const AIInputBar: React.FC<AIInputBarProps> = React.memo(({ className, is
const hasSelection = allContent.length > 0;
addPromptHistory(prompt.trim(), hasSelection);
}
// 清空输入并关闭面板
// 清空输入,保持面板打开以便用户继续创作
setPrompt('');
setSelectedContent([]);
setUploadedContent([]);
setIsFocused(false);
inputRef.current?.blur();
// 启动 1 秒冷却定时器,之后允许用户继续输入
if (submitCooldownRef.current) {
clearTimeout(submitCooldownRef.current);
}
console.log('[AIInputBar] SW execution success, starting 1s cooldown timer');
submitCooldownRef.current = setTimeout(() => {
console.log('[AIInputBar] 1s cooldown expired: setting isSubmitting=false');
setIsSubmitting(false);
submitCooldownRef.current = null;
}, 1000);
return; // 提前返回
}
} catch (swError) {
@@ -1294,13 +1316,11 @@ export const AIInputBar: React.FC<AIInputBarProps> = React.memo(({ className, is
}
}
// 清空输入并关闭面板
// 清空输入,保持面板打开以便用户继续创作
setPrompt('');
setSelectedContent([]);
setUploadedContent([]); // 同时清空用户上传内容
setIsFocused(false);
// 让输入框失去焦点
inputRef.current?.blur();
// 不关闭面板,让用户可以继续输入
} catch (error) {
console.error('Failed to create generation task:', error);
// 中止工作流
@@ -1308,15 +1328,17 @@ export const AIInputBar: React.FC<AIInputBarProps> = React.memo(({ className, is
// 出错时立即允许重试
setIsSubmitting(false);
}
// 成功提交后,3秒内不允许重复提交(防止误操作双击)
// 成功提交后,1秒内不允许重复提交(防止误操作双击)
// 清除之前的定时器
if (submitCooldownRef.current) {
clearTimeout(submitCooldownRef.current);
}
console.log('[AIInputBar] Starting 1s cooldown timer');
submitCooldownRef.current = setTimeout(() => {
console.log('[AIInputBar] 1s cooldown expired: setting isSubmitting=false');
setIsSubmitting(false);
submitCooldownRef.current = null;
}, 3000);
}, 1000);
}, [prompt, allContent, isSubmitting, selectedModel, workflowControl, submitWorkflowToSW, addPromptHistory, selectedSize]);
// 处理工作流重试(从指定步骤开始)
@@ -77,6 +77,8 @@ export const Minimap: React.FC<MinimapProps> = ({
offsetX: board.viewport.offsetX,
offsetY: board.viewport.offsetY,
});
// 是否已完成初始化(跳过首次 viewport 变化检测,避免页面加载时误触发)
const initializedRef = useRef<boolean>(false);
/**
* 获取所有元素的边界
@@ -179,6 +181,13 @@ export const Minimap: React.FC<MinimapProps> = ({
offsetX: current.offsetX,
offsetY: current.offsetY,
};
// 首次检测到变化时,只更新 ref,不展开小地图(跳过初始化阶段的 viewport 变化)
if (!initializedRef.current) {
initializedRef.current = true;
return;
}
lastInteractionRef.current = Date.now();
// 有交互时自动展开小地图
@@ -322,7 +322,7 @@ export const WorkZoneTransforms = {
Transforms.insertNode(board, workzoneElement, [board.children.length]);
});
console.log('[WorkZone] Inserted (without history):', workzoneElement.id);
// console.log('[WorkZone] Inserted (without history):', workzoneElement.id);
return workzoneElement;
},
@@ -352,7 +352,7 @@ export const WorkZoneTransforms = {
PlaitHistoryBoard.withoutSaving(board, () => {
Transforms.removeNode(board, [index]);
});
console.log('[WorkZone] Removed (without history):', elementId);
// console.log('[WorkZone] Removed (without history):', elementId);
}
},
@@ -116,6 +116,7 @@ export async function handleSplitAndInsertTask(
const insertionPoint = config.insertionPoint || getDefaultInsertionPoint();
if (result.success) {
console.log(`[MediaResultHandler] Split success, calling completePostProcessing for task ${taskId}`);
workflowCompletionService.completePostProcessing(
taskId,
result.count,
@@ -128,6 +129,7 @@ export async function handleSplitAndInsertTask(
const insertResult = await insertImageGroup([url], insertionPoint);
if (insertResult.success) {
console.log(`[MediaResultHandler] Fallback insert success, calling completePostProcessing for task ${taskId}`);
workflowCompletionService.completePostProcessing(taskId, 1, insertionPoint);
return { success: true, count: 1 };
} else {
@@ -108,8 +108,8 @@ export class SWCapabilitiesHandler {
*/
private findWorkZoneForMCP(board: PlaitBoard, mcpTool: string): PlaitWorkZone | null {
const allWorkZones = WorkZoneTransforms.getAllWorkZones(board);
console.log('[SWCapabilities] findWorkZoneForMCP:', mcpTool, 'total WorkZones:', allWorkZones.length);
// console.log('[SWCapabilities] findWorkZoneForMCP:', mcpTool, 'total WorkZones:', allWorkZones.length);
if (allWorkZones.length === 0) {
return null;
}
@@ -118,14 +118,14 @@ export class SWCapabilitiesHandler {
for (const workzone of allWorkZones) {
const hasMatchingStep = workzone.workflow.steps?.some(step => step.mcp === mcpTool);
if (hasMatchingStep) {
console.log('[SWCapabilities] Found WorkZone with matching MCP step:', workzone.id);
// console.log('[SWCapabilities] Found WorkZone with matching MCP step:', workzone.id);
return workzone;
}
}
// Fallback: return the most recent WorkZone
const sortedWorkZones = allWorkZones.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
console.log('[SWCapabilities] Using most recent WorkZone:', sortedWorkZones[0]?.id);
// console.log('[SWCapabilities] Using most recent WorkZone:', sortedWorkZones[0]?.id);
return sortedWorkZones[0] || null;
}
@@ -209,7 +209,7 @@ export class SWCapabilitiesHandler {
return { success: false, error: '缺少 mermaid 参数', type: 'error' };
}
console.log('[SWCapabilities] handleMermaid called, mermaid length:', mermaid.length);
// console.log('[SWCapabilities] handleMermaid called, mermaid length:', mermaid.length);
try {
// Dynamic import mermaid-to-drawnix
@@ -217,7 +217,7 @@ export class SWCapabilitiesHandler {
// Extract mermaid code from markdown code block if present
const mermaidCode = this.extractCodeBlock(mermaid, 'mermaid');
console.log('[SWCapabilities] Extracted mermaid code length:', mermaidCode.length);
// console.log('[SWCapabilities] Extracted mermaid code length:', mermaidCode.length);
// Parse mermaid to drawnix elements
let result;
@@ -235,15 +235,15 @@ export class SWCapabilitiesHandler {
// Find WorkZone associated with this mermaid operation
const targetWorkZone = this.findWorkZoneForMCP(board, 'insert_mermaid');
console.log('[SWCapabilities] Target WorkZone:', targetWorkZone?.id, 'expectedInsertPosition:', targetWorkZone?.expectedInsertPosition);
// console.log('[SWCapabilities] Target WorkZone:', targetWorkZone?.id, 'expectedInsertPosition:', targetWorkZone?.expectedInsertPosition);
// Get viewport center as insertion point for better centering
const viewportCenter = this.getViewportCenter(board);
console.log('[SWCapabilities] Viewport center:', viewportCenter);
// console.log('[SWCapabilities] Viewport center:', viewportCenter);
// Insert elements to canvas at viewport center
const insertResult = this.insertElementsToCanvasAtPoint(board, elements, viewportCenter);
console.log('[SWCapabilities] Insert result:', insertResult);
// console.log('[SWCapabilities] Insert result:', insertResult);
if (insertResult.success) {
// Center the inserted elements in viewport after a short delay
@@ -253,10 +253,10 @@ export class SWCapabilitiesHandler {
if (targetWorkZone) {
// Remove the WorkZone after successful insertion
console.log('[SWCapabilities] Removing WorkZone:', targetWorkZone.id);
// console.log('[SWCapabilities] Removing WorkZone:', targetWorkZone.id);
setTimeout(() => {
WorkZoneTransforms.removeWorkZone(board, targetWorkZone!.id);
console.log('[SWCapabilities] WorkZone removed successfully');
// console.log('[SWCapabilities] WorkZone removed successfully');
// Dispatch event to notify AI input bar that generation is complete
window.dispatchEvent(new CustomEvent('ai-generation-complete', {
@@ -296,7 +296,7 @@ export class SWCapabilitiesHandler {
return { success: false, error: '缺少 markdown 参数', type: 'error' };
}
console.log('[SWCapabilities] handleMindmap called, markdown length:', markdown.length);
// console.log('[SWCapabilities] handleMindmap called, markdown length:', markdown.length);
try {
// Dynamic import markdown-to-drawnix
@@ -304,7 +304,7 @@ export class SWCapabilitiesHandler {
// Extract markdown code
const markdownCode = this.extractCodeBlock(markdown, 'markdown');
console.log('[SWCapabilities] Extracted markdown code length:', markdownCode.length);
// console.log('[SWCapabilities] Extracted markdown code length:', markdownCode.length);
// Parse markdown to mindmap element
let mindElement;
@@ -323,15 +323,15 @@ export class SWCapabilitiesHandler {
// Find WorkZone associated with this mindmap operation
const targetWorkZone = this.findWorkZoneForMCP(board, 'insert_mindmap');
console.log('[SWCapabilities] Target WorkZone:', targetWorkZone?.id, 'expectedInsertPosition:', targetWorkZone?.expectedInsertPosition);
// console.log('[SWCapabilities] Target WorkZone:', targetWorkZone?.id, 'expectedInsertPosition:', targetWorkZone?.expectedInsertPosition);
// Get viewport center as insertion point for better centering
const viewportCenter = this.getViewportCenter(board);
console.log('[SWCapabilities] Viewport center:', viewportCenter);
// console.log('[SWCapabilities] Viewport center:', viewportCenter);
// Insert to canvas at viewport center
const insertResult = this.insertElementsToCanvasAtPoint(board, [mindElement], viewportCenter);
console.log('[SWCapabilities] Insert result:', insertResult);
// console.log('[SWCapabilities] Insert result:', insertResult);
if (insertResult.success) {
// Center the inserted mindmap in viewport after a short delay
@@ -341,10 +341,10 @@ export class SWCapabilitiesHandler {
if (targetWorkZone) {
// Remove the WorkZone after successful insertion
console.log('[SWCapabilities] Removing WorkZone:', targetWorkZone.id);
// console.log('[SWCapabilities] Removing WorkZone:', targetWorkZone.id);
setTimeout(() => {
WorkZoneTransforms.removeWorkZone(board, targetWorkZone!.id);
console.log('[SWCapabilities] WorkZone removed successfully');
// console.log('[SWCapabilities] WorkZone removed successfully');
// Dispatch event to notify AI input bar that generation is complete
window.dispatchEvent(new CustomEvent('ai-generation-complete', {
@@ -744,7 +744,7 @@ export class SWCapabilitiesHandler {
insertPoint: Point
): { success: boolean; elementsCount?: number; error?: string } {
try {
console.log('[SWCapabilities] insertElementsToCanvasAtPoint called, point:', insertPoint, 'elements count:', elements.length);
// console.log('[SWCapabilities] insertElementsToCanvasAtPoint called, point:', insertPoint, 'elements count:', elements.length);
// Use board.insertFragment to correctly insert elements
// This handles element positioning and ID generation properly
@@ -759,7 +759,7 @@ export class SWCapabilitiesHandler {
scrollToPointIfNeeded(board, insertPoint);
});
console.log('[SWCapabilities] Elements inserted successfully at point:', insertPoint);
// console.log('[SWCapabilities] Elements inserted successfully at point:', insertPoint);
return { success: true, elementsCount: elements.length };
} catch (error: any) {
console.error('[SWCapabilities] insertElementsToCanvasAtPoint failed:', error);
@@ -786,7 +786,7 @@ export class SWCapabilitiesHandler {
const centerX = origination[0] + containerRect.width / (2 * zoom);
const centerY = origination[1] + containerRect.height / (2 * zoom);
console.log('[SWCapabilities] getViewportCenter:', { centerX, centerY, zoom, origination });
// console.log('[SWCapabilities] getViewportCenter:', { centerX, centerY, zoom, origination });
return [centerX, centerY];
} catch (error) {
console.warn('[SWCapabilities] Error getting viewport center:', error);
@@ -811,7 +811,7 @@ export class SWCapabilitiesHandler {
// Calculate the bounding rectangle of all inserted elements
const boundingRect = getRectangleByElements(board, insertedElements as any[], false);
console.log('[SWCapabilities] Inserted elements bounding rect:', boundingRect);
// console.log('[SWCapabilities] Inserted elements bounding rect:', boundingRect);
// Get viewport dimensions
const boardContainer = PlaitBoardUtils.getBoardContainer(board);
@@ -831,14 +831,14 @@ export class SWCapabilitiesHandler {
// Don't zoom in too much if elements are small
optimalZoom = Math.min(Math.max(optimalZoom, 0.2), 1.5);
console.log('[SWCapabilities] Calculated optimal zoom:', {
boundingRect,
availableWidth,
availableHeight,
zoomX,
zoomY,
optimalZoom,
});
// console.log('[SWCapabilities] Calculated optimal zoom:', {
// boundingRect,
// availableWidth,
// availableHeight,
// zoomX,
// zoomY,
// optimalZoom,
// });
// Calculate the center of the inserted elements
const elementsCenterX = boundingRect.x + boundingRect.width / 2;
@@ -848,11 +848,11 @@ export class SWCapabilitiesHandler {
const newOriginationX = elementsCenterX - containerRect.width / (2 * optimalZoom);
const newOriginationY = elementsCenterY - containerRect.height / (2 * optimalZoom);
console.log('[SWCapabilities] Centering and fitting elements in viewport:', {
elementsCenter: [elementsCenterX, elementsCenterY],
newOrigination: [newOriginationX, newOriginationY],
optimalZoom,
});
// console.log('[SWCapabilities] Centering and fitting elements in viewport:', {
// elementsCenter: [elementsCenterX, elementsCenterY],
// newOrigination: [newOriginationX, newOriginationY],
// optimalZoom,
// });
// Update viewport with optimal zoom and centered position
BoardTransforms.updateViewport(board, [newOriginationX, newOriginationY], optimalZoom);
@@ -163,6 +163,7 @@ class WorkflowCompletionService {
this.postProcessingResults.set(taskId, result);
console.log(`[WorkflowCompletion] Emitting postProcessingCompleted event for task ${taskId}`);
this.completionEvents$.next({
taskId,
batchId: this.taskToBatch.get(taskId),