feat: add element inspector with chat integration
- Implement element inspector tool for preview iframe with hover/click detection - Add inspector panel UI to display element details and styles - Integrate selected elements into chat messages for reference - Style improvements for chat messages and scroll behavior - Add inspector script injection to preview iframe - Support element selection and context in chat prompts -Redesign Messgaes, Workbench and Header for a more refined look allowing more workspace in view
This commit is contained in:
126
app/components/workbench/Inspector.tsx
Normal file
126
app/components/workbench/Inspector.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface InspectorProps {
|
||||
isActive: boolean;
|
||||
iframeRef: React.RefObject<HTMLIFrameElement>;
|
||||
onElementSelect: (elementInfo: ElementInfo) => void;
|
||||
}
|
||||
|
||||
export interface ElementInfo {
|
||||
displayText: string;
|
||||
tagName: string;
|
||||
className: string;
|
||||
id: string;
|
||||
textContent: string;
|
||||
styles: Record<string, string>; // Changed from CSSStyleDeclaration
|
||||
rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const Inspector = ({ isActive, iframeRef, onElementSelect }: InspectorProps) => {
|
||||
const [hoveredElement, setHoveredElement] = useState<ElementInfo | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !iframeRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iframe = iframeRef.current;
|
||||
|
||||
// Listen for messages from the iframe
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === 'INSPECTOR_HOVER') {
|
||||
const elementInfo = event.data.elementInfo;
|
||||
|
||||
// Adjust coordinates relative to iframe position
|
||||
const iframeRect = iframe.getBoundingClientRect();
|
||||
elementInfo.rect.x += iframeRect.x;
|
||||
elementInfo.rect.y += iframeRect.y;
|
||||
elementInfo.rect.top += iframeRect.y;
|
||||
elementInfo.rect.left += iframeRect.x;
|
||||
|
||||
setHoveredElement(elementInfo);
|
||||
} else if (event.data.type === 'INSPECTOR_CLICK') {
|
||||
const elementInfo = event.data.elementInfo;
|
||||
|
||||
// Adjust coordinates relative to iframe position
|
||||
const iframeRect = iframe.getBoundingClientRect();
|
||||
elementInfo.rect.x += iframeRect.x;
|
||||
elementInfo.rect.y += iframeRect.y;
|
||||
elementInfo.rect.top += iframeRect.y;
|
||||
elementInfo.rect.left += iframeRect.x;
|
||||
|
||||
onElementSelect(elementInfo);
|
||||
} else if (event.data.type === 'INSPECTOR_LEAVE') {
|
||||
setHoveredElement(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
// Send activation message to iframe
|
||||
const sendActivationMessage = () => {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'INSPECTOR_ACTIVATE',
|
||||
active: isActive,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Try to send activation message immediately and on load
|
||||
sendActivationMessage();
|
||||
iframe.addEventListener('load', sendActivationMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', handleMessage);
|
||||
iframe.removeEventListener('load', sendActivationMessage);
|
||||
|
||||
// Deactivate inspector in iframe
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'INSPECTOR_ACTIVATE',
|
||||
active: false,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [isActive, iframeRef, onElementSelect]);
|
||||
|
||||
// Render overlay for hovered element
|
||||
return (
|
||||
<>
|
||||
{isActive && hoveredElement && (
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className="fixed pointer-events-none z-50 border-2 border-blue-500 bg-blue-500/10"
|
||||
style={{
|
||||
left: hoveredElement.rect.x,
|
||||
top: hoveredElement.rect.y,
|
||||
width: hoveredElement.rect.width,
|
||||
height: hoveredElement.rect.height,
|
||||
}}
|
||||
>
|
||||
{/* Element info tooltip */}
|
||||
<div className="absolute -top-8 left-0 bg-gray-900 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
|
||||
{hoveredElement.tagName.toLowerCase()}
|
||||
{hoveredElement.id && `#${hoveredElement.id}`}
|
||||
{hoveredElement.className && `.${hoveredElement.className.split(' ')[0]}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user