diff --git a/apps/canvas/front/src/components/ChatBubble.tsx b/apps/canvas/front/src/components/ChatBubble.tsx
new file mode 100644
index 0000000..74e3605
--- /dev/null
+++ b/apps/canvas/front/src/components/ChatBubble.tsx
@@ -0,0 +1,191 @@
+import React, { useState, useRef, useEffect, useCallback } from "react";
+
+interface ChatBubbleProps {
+	agentName: string;
+	agentUrl: string;
+	initialPosition?: { bottom?: number; right?: number; top?: number; left?: number };
+}
+
+const MIN_WIDTH = 200;
+const MIN_HEIGHT = 150;
+const DEFAULT_WIDTH = 350;
+const DEFAULT_HEIGHT = 500;
+
+export function ChatBubble({ agentName, agentUrl, initialPosition = { bottom: 20, right: 20 } }: ChatBubbleProps) {
+	const [isExpanded, setIsExpanded] = useState(false);
+	const [position, setPosition] = useState(initialPosition);
+	const [size, setSize] = useState({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
+	const [isDragging, setIsDragging] = useState(false);
+	const [isResizing, setIsResizing] = useState(false);
+	const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
+	const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 0, height: 0 });
+
+	const bubbleRef = useRef<HTMLDivElement>(null);
+
+	const toggleExpansion = useCallback(
+		(e: React.MouseEvent) => {
+			if (isResizing) {
+				return;
+			}
+			if (
+				isExpanded &&
+				(e.target === e.currentTarget || (e.target as HTMLElement).closest("button") === e.currentTarget)
+			) {
+				e.stopPropagation();
+			}
+			setIsExpanded((prev) => !prev);
+			if (!isExpanded) {
+				setSize({ width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT });
+			}
+		},
+		[isExpanded, isResizing],
+	);
+
+	const handleDragMouseDown = useCallback((e: React.MouseEvent) => {
+		if (!bubbleRef.current || (e.target as HTMLElement).closest("button")) {
+			return;
+		}
+		setIsDragging(true);
+		const rect = bubbleRef.current.getBoundingClientRect();
+		setDragOffset({
+			x: e.clientX - rect.left,
+			y: e.clientY - rect.top,
+		});
+		e.preventDefault();
+	}, []);
+
+	const handleDragMouseMove = useCallback(
+		(e: MouseEvent) => {
+			if (!isDragging) return;
+			setPosition({
+				top: e.clientY - dragOffset.y,
+				left: e.clientX - dragOffset.x,
+				bottom: undefined,
+				right: undefined,
+			});
+		},
+		[isDragging, dragOffset],
+	);
+
+	const handleDragMouseUp = useCallback(() => {
+		setIsDragging(false);
+	}, []);
+
+	const handleResizeMouseDown = useCallback(
+		(e: React.MouseEvent) => {
+			e.stopPropagation();
+			setIsResizing(true);
+			setResizeStart({
+				x: e.clientX,
+				y: e.clientY,
+				width: size.width,
+				height: size.height,
+			});
+			e.preventDefault();
+		},
+		[size],
+	);
+
+	const handleResizeMouseMove = useCallback(
+		(e: MouseEvent) => {
+			if (!isResizing) return;
+			const newWidth = Math.max(MIN_WIDTH, resizeStart.width + (e.clientX - resizeStart.x));
+			const newHeight = Math.max(MIN_HEIGHT, resizeStart.height + (e.clientY - resizeStart.y));
+			setSize({ width: newWidth, height: newHeight });
+		},
+		[isResizing, resizeStart],
+	);
+
+	const handleResizeMouseUp = useCallback(() => {
+		setIsResizing(false);
+	}, []);
+
+	useEffect(() => {
+		const activeDragging = isDragging;
+		const activeResizing = isResizing;
+
+		const globalMouseMove = (e: MouseEvent) => {
+			if (activeDragging) handleDragMouseMove(e);
+			if (activeResizing) handleResizeMouseMove(e);
+		};
+		const globalMouseUp = () => {
+			if (activeDragging) handleDragMouseUp();
+			if (activeResizing) handleResizeMouseUp();
+		};
+
+		if (activeDragging || activeResizing) {
+			window.addEventListener("mousemove", globalMouseMove);
+			window.addEventListener("mouseup", globalMouseUp);
+		} else {
+			window.removeEventListener("mousemove", globalMouseMove);
+			window.removeEventListener("mouseup", globalMouseUp);
+		}
+		return () => {
+			window.removeEventListener("mousemove", globalMouseMove);
+			window.removeEventListener("mouseup", globalMouseUp);
+		};
+	}, [isDragging, isResizing, handleDragMouseMove, handleDragMouseUp, handleResizeMouseMove, handleResizeMouseUp]);
+
+	const baseBubbleClasses = `fixed border shadow-lg z-[1000] flex flex-col bg-card text-card-foreground p-3`;
+	const collapsedClasses = `min-w-[150px] h-auto rounded-lg items-start`;
+	const expandedDynamicClasses = `rounded-lg items-center`;
+
+	const bubbleDynamicClasses = `${baseBubbleClasses} ${isExpanded ? expandedDynamicClasses : collapsedClasses}`;
+
+	const headerClasses = "w-full flex justify-between items-center cursor-grab select-none";
+	const iframeContainerClasses = "w-full flex-grow mt-2.5 relative";
+	const iframeClasses = "w-full h-full border-none rounded-b-md";
+	const buttonClasses = "bg-transparent border-none text-card-foreground p-1 leading-none cursor-pointer";
+	const resizeHandleClasses =
+		"absolute bottom-0 right-0 w-4 h-4 cursor-se-resize bg-gray-400 opacity-50 hover:opacity-100";
+
+	const bubbleStyle: React.CSSProperties = {
+		top: position.top !== undefined ? `${position.top}px` : undefined,
+		left: position.left !== undefined ? `${position.left}px` : undefined,
+		bottom: position.bottom !== undefined && position.top === undefined ? `${position.bottom}px` : undefined,
+		right: position.right !== undefined && position.left === undefined ? `${position.right}px` : undefined,
+		width: isExpanded ? `${size.width}px` : undefined,
+		height: isExpanded ? `${size.height}px` : undefined,
+	};
+
+	return (
+		<div
+			ref={bubbleRef}
+			className={bubbleDynamicClasses}
+			style={bubbleStyle}
+			onClick={!isExpanded && !isDragging && !isResizing ? toggleExpansion : undefined}
+		>
+			<div
+				className={headerClasses}
+				onMouseDown={handleDragMouseDown}
+				onDoubleClick={(e) => {
+					e.stopPropagation();
+					toggleExpansion(e);
+				}}
+			>
+				<span className="font-semibold truncate pr-2">{agentName}</span>
+				<button
+					onClick={(e) => {
+						e.stopPropagation();
+						toggleExpansion(e);
+					}}
+					className={buttonClasses}
+					aria-label={isExpanded ? "Collapse chat" : "Expand chat"}
+				>
+					{isExpanded ? "▼" : "▲"}
+				</button>
+			</div>
+			{isExpanded && (
+				<div className={iframeContainerClasses}>
+					<iframe
+						src={agentUrl}
+						title={agentName}
+						className={iframeClasses}
+						style={{ pointerEvents: isDragging || isResizing ? "none" : "auto" }}
+					/>
+					<div className={resizeHandleClasses} onMouseDown={handleResizeMouseDown} />
+				</div>
+			)}
+		</div>
+	);
+}
