Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const OverlayChatInput = observer(({
return (
<div
className={cn(
'rounded-xl backdrop-blur-lg transition-all duration-300',
'rounded-xl backdrop-blur-lg transition-all duration-150',
'shadow-xl shadow-background-secondary/50',
inputState.isInputting
? 'bg-background/80 border shadow-xl shadow-background-secondary/50 p-1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ export const OverlayButtons = observer(() => {
}, [chatPosition.x, chatPosition.y]);

const animationClass =
'origin-center opacity-0 -translate-y-2 transition-all duration-200';
'origin-center opacity-0 -translate-y-1 transition-all duration-100';

useEffect(() => {
if (domId) {
requestAnimationFrame(() => {
const element = document.querySelector(`[data-element-id="${domId}"]`);
if (element) {
element.classList.remove('scale-[0.2]', 'opacity-0', '-translate-y-2');
element.classList.remove('scale-[0.2]', 'opacity-0', '-translate-y-1');
element.classList.add('scale-100', 'opacity-100', 'translate-y-0');
}
});
Expand All @@ -72,7 +72,7 @@ export const OverlayButtons = observer(() => {
transform: 'translate(-50%, 0)',
transformOrigin: 'center center',
pointerEvents: 'auto',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transition: 'all 0.15s ease-out',
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const Overlay = observer(() => {
id={EditorAttributes.OVERLAY_CONTAINER_ID}
className={cn(
'absolute top-0 left-0 h-0 w-0 pointer-events-none',
editorEngine.state.shouldHideOverlay ? 'opacity-0' : 'opacity-100 transition-opacity duration-150',
editorEngine.state.shouldHideOverlay ? 'opacity-0' : 'opacity-100 transition-opacity duration-75',
editorEngine.state.editorMode === EditorMode.PREVIEW && 'hidden',
)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,16 @@ export const EditorBar = observer(({ availableWidth }: { availableWidth?: number
return (
<DropdownManagerProvider>
<motion.div
initial={{ opacity: 0, y: 20 }}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
exit={{ opacity: 0, y: 8 }}
className={cn(
'flex flex-col border-[0.5px] border-border p-1 px-1 bg-background rounded-xl backdrop-blur drop-shadow-xl z-50 overflow-hidden',
editorEngine.state.editorMode === EditorMode.PREVIEW && !windowSelected && 'hidden',
)}
transition={{
type: 'spring',
bounce: 0.1,
duration: 0.4,
stiffness: 200,
damping: 25,
duration: 0.15,
ease: 'easeOut',
}}
>
{getTopBar()}
Expand Down
19 changes: 17 additions & 2 deletions apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,24 @@ export const useStartProject = () => {

useEffect(() => {
if (tabState === 'reactivated') {
editorEngine.activeSandbox.session.reconnect(editorEngine.projectId, user?.id);
// Only warm up on reactivation, not on initial load
// The 'reactivated' state only occurs when returning to a previously inactive tab
const warmUpConnection = async () => {
// Reconnect sandbox session
await editorEngine.activeSandbox.session.reconnect(editorEngine.projectId, user?.id);

// Clear any stale overlay state and refresh selected elements only
if (editorEngine.elements.selected.length > 0) {
// Re-trigger selection to refresh the outline
const selected = [...editorEngine.elements.selected];
editorEngine.overlay.clearUI();
editorEngine.elements.click(selected);
}
};

warmUpConnection().catch(console.error);
}
}, [tabState]);
}, [tabState]); // Reduce dependencies to prevent unnecessary re-runs

useEffect(() => {
setError(userError?.message ?? canvasError?.message ?? conversationsError?.message ?? creationRequestError?.message ?? null);
Expand Down
76 changes: 70 additions & 6 deletions apps/web/client/src/components/store/editor/chat/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import type { EditorEngine } from '../engine';
export class ChatContext {
context: MessageContext[] = [];
private selectedReactionDisposer?: () => void;
private pendingContextUpdate?: number;
private contextUpdateAbortController?: AbortController;

constructor(
private editorEngine: EditorEngine,
) {
Expand All @@ -25,7 +28,45 @@ export class ChatContext {
init() {
this.selectedReactionDisposer = reaction(
() => this.editorEngine.elements.selected,
() => this.getChatContext().then((context) => (this.context = context)),
() => {
// Skip on empty selection to avoid unnecessary work
if (this.editorEngine.elements.selected.length === 0) {
this.context = [];
return;
}

// Cancel any pending context update
if (this.pendingContextUpdate) {
clearTimeout(this.pendingContextUpdate);
}

// Abort any in-flight async operations
if (this.contextUpdateAbortController) {
this.contextUpdateAbortController.abort();
}

// Create new abort controller for this update
this.contextUpdateAbortController = new AbortController();
const signal = this.contextUpdateAbortController.signal;

// Defer context update to prevent blocking UI
// Use setTimeout with longer delay for lower priority
this.pendingContextUpdate = window.setTimeout(() => {
if (!signal.aborted) {
this.getChatContext().then((context) => {
if (!signal.aborted) {
this.context = context;
}
}).catch((error) => {
if (!signal.aborted) {
console.error('Error updating chat context:', error);
}
});
}
this.pendingContextUpdate = undefined;
}, 300); // Delay context update by 300ms to prioritize UI
},
{ delay: 150 } // Increase delay to further batch rapid changes
);
}

Expand Down Expand Up @@ -85,17 +126,28 @@ export class ChatContext {
highlightedContext.forEach(highlight => {
filePathToBranch.set(highlight.path, highlight.branchId);
});

// Limit the number of file reads to prevent memory issues
const MAX_FILES_TO_READ = 5;
const filesToRead = Array.from(filePathToBranch.entries()).slice(0, MAX_FILES_TO_READ);

for (const [filePath, branchId] of filePathToBranch) {
for (const [filePath, branchId] of filesToRead) {
const file = await this.editorEngine.activeSandbox.readFile(filePath);
if (file === null || file.type === 'binary') {
continue;
}

// Limit file content size to prevent huge memory usage
const MAX_CONTENT_LENGTH = 50000; // ~50KB per file
const truncatedContent = file.content.length > MAX_CONTENT_LENGTH
? file.content.substring(0, MAX_CONTENT_LENGTH) + '\n... [truncated]'
: file.content;

fileContext.push({
type: MessageContextType.FILE,
displayName: filePath,
path: filePath,
content: file.content,
content: truncatedContent,
branchId: branchId,
});
}
Expand Down Expand Up @@ -133,19 +185,19 @@ export class ChatContext {
for (const node of selected) {
const oid = node.oid;
if (!oid) {
console.error('No oid found for node', node);
// Removed console.error to prevent log spam
continue;
}

const codeBlock = await this.editorEngine.templateNodes.getCodeBlock(oid);
if (codeBlock === null) {
console.error('No code block found for node', node);
// Removed console.error to prevent log spam
continue;
}

const templateNode = this.editorEngine.templateNodes.getTemplateNode(oid);
if (!templateNode) {
console.error('No template node found for node', node);
// Removed console.error to prevent log spam
continue;
}

Expand Down Expand Up @@ -283,6 +335,18 @@ export class ChatContext {
}

clear() {
// Cancel pending updates
if (this.pendingContextUpdate) {
clearTimeout(this.pendingContextUpdate);
this.pendingContextUpdate = undefined;
}

// Abort in-flight operations
if (this.contextUpdateAbortController) {
this.contextUpdateAbortController.abort();
this.contextUpdateAbortController = undefined;
}

this.selectedReactionDisposer?.();
this.selectedReactionDisposer = undefined;
this.context = [];
Expand Down
103 changes: 76 additions & 27 deletions apps/web/client/src/components/store/editor/element/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import type { CoreElementType, DomElement, DynamicType } from '@onlook/models';
import type { RemoveElementAction } from '@onlook/models/actions';
import { toast } from '@onlook/ui/sonner';
import { makeAutoObservable } from 'mobx';
import { throttle } from 'lodash';
import { makeAutoObservable, runInAction } from 'mobx';
import type { EditorEngine } from '../engine';
import type { FrameData } from '../frames';
import { adaptRectToCanvas } from '../overlay/utils';

// Throttle console errors to prevent log spam during rapid clicks
const throttledError = throttle((message: string, ...args: any[]) => {
console.error(message, ...args);
}, 1000);

export class ElementsManager {
private _hovered: DomElement | undefined;
private _selected: DomElement[] = [];
private lastClickTime: number = 0;
private isInitialized: boolean = false;

constructor(private editorEngine: EditorEngine) {
makeAutoObservable(this);
Expand All @@ -29,7 +37,7 @@ export class ElementsManager {
mouseover(domEl: DomElement) {
const frameData = this.editorEngine.frames.get(domEl.frameId);
if (!frameData?.view) {
console.error('No frame view found');
throttledError('No frame view found');
return;
}
if (this._hovered?.domId && this._hovered.domId === domEl.domId) {
Expand Down Expand Up @@ -60,30 +68,71 @@ export class ElementsManager {
}

click(domEls: DomElement[]) {
this.editorEngine.overlay.state.removeClickRects();
this.clearSelectedElements();

for (const domEl of domEls) {
const frameData = this.editorEngine.frames.get(domEl.frameId);
if (!frameData) {
console.error('Frame data not found');
continue;
// Skip warmup logic on first clicks to improve initial load
let needsWarmup = false;
if (this.isInitialized) {
const now = performance.now();
const timeSinceLastClick = now - this.lastClickTime;
this.lastClickTime = now;
// Only check for warmup after initialization
needsWarmup = timeSinceLastClick > 5000;
} else {
this.isInitialized = true;
this.lastClickTime = performance.now();
}

// Batch all synchronous updates for the outline rendering
runInAction(() => {
// Immediately update the visual outline for instant feedback
this.editorEngine.overlay.state.removeClickRects();

// Process outline rendering first for immediate visual feedback
for (const domEl of domEls) {
const frameData = this.editorEngine.frames.get(domEl.frameId);
if (!frameData) {
throttledError('Frame data not found');
continue;
}
const { view } = frameData;
if (!view) {
throttledError('No frame view found');
continue;
}
const adjustedRect = adaptRectToCanvas(domEl.rect, view);
const isComponent = !!domEl.instanceId;
this.editorEngine.overlay.state.addClickRect(
adjustedRect,
domEl.styles,
isComponent,
domEl.domId,
);
}
const { view } = frameData;
if (!view) {
console.error('No frame view found');
continue;
});

// If we need warmup, do a quick no-op to wake up the engine
if (needsWarmup && domEls.length > 0) {
// Quick check to ensure frame is active
const firstElement = domEls[0];
if (firstElement) {
const frameData = this.editorEngine.frames.get(firstElement.frameId);
if (frameData?.view) {
try {
// Just checking if loading helps wake up the view
frameData.view.isLoading();
} catch {
// Ignore errors during warmup
}
}
}
const adjustedRect = adaptRectToCanvas(domEl.rect, view);
const isComponent = !!domEl.instanceId;
this.editorEngine.overlay.state.addClickRect(
adjustedRect,
domEl.styles,
isComponent,
domEl.domId,
);
this._selected.push(domEl);
}

// Update selected elements in a separate action to control reaction timing
runInAction(() => {
this.clearSelectedElements();
for (const domEl of domEls) {
this._selected.push(domEl);
}
});
}

setHoveredElement(element: DomElement) {
Expand All @@ -95,7 +144,7 @@ export class ElementsManager {
}

emitError(error: string) {
console.error(error);
throttledError(error);
toast.error('Cannot delete element', { description: error });
}

Expand All @@ -109,7 +158,7 @@ export class ElementsManager {
const frameId = selectedEl.frameId;
const frameData = this.editorEngine.frames.get(frameId);
if (!frameData?.view) {
console.error('No frame view found');
throttledError('No frame view found');
return;
}
const { shouldDelete, error } = await this.shouldDelete(selectedEl, frameData);
Expand Down Expand Up @@ -143,7 +192,7 @@ export class ElementsManager {
removeAction.codeBlock = codeBlock;

this.editorEngine.action.run(removeAction).catch((err) => {
console.error('Error deleting element', err);
throttledError('Error deleting element', err);
});
}
}
Expand All @@ -159,7 +208,7 @@ export class ElementsManager {

if (!instanceId) {
if (!frameData.view) {
console.error('No frame view found');
throttledError('No frame view found');
return {
shouldDelete: false,
error: 'No frame view found',
Expand Down
Loading