agent-Specialization/scripts/liquid-glass-demo/components/DynamicFrameLayout.tsx
JOJO 2f75c1c8bb feat: stable version before virtual monitor timing fix
Current status includes:
- Virtual monitor surface and components
- Monitor store for state management
- Tool call animations and transitions
- Liquid glass shader integration

Known issue to fix: Tool status display timing - "正在xx" appears
after tool execution completes instead of when tool call starts.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 17:12:12 +08:00

323 lines
12 KiB
TypeScript

"use client"
import { useState } from "react"
import { motion } from "framer-motion"
import { FrameComponent } from "./FrameComponent"
import { Slider } from "@/components/ui/slider"
import { Button } from "@/components/ui/button"
import { Switch } from "@/components/ui/switch"
const GRID_SIZE = 12
const CELL_SIZE = 60 // pixels per grid cell
interface Frame {
id: number
video: string
defaultPos: { x: number; y: number; w: number; h: number }
corner: string
edgeHorizontal: string
edgeVertical: string
mediaSize: number
borderThickness: number
borderSize: number
autoplayMode: "all" | "hover"
isHovered: boolean
}
const initialFrames: Frame[] = [
{
id: 1,
video: "https://static.cdn-luma.com/files/981e483f71aa764b/Company%20Thing%20Exported.mp4",
defaultPos: { x: 0, y: 0, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/bcf576df9c38b05f/1_corner_update.png",
edgeHorizontal: "https://static.cdn-luma.com/files/bcf576df9c38b05f/1_vert_update.png",
edgeVertical: "https://static.cdn-luma.com/files/bcf576df9c38b05f/1_hori_update.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 2,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/WebGL%20Exported%20(1).mp4",
defaultPos: { x: 4, y: 0, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/bcf576df9c38b05f/2_corner_update.png",
edgeHorizontal: "https://static.cdn-luma.com/files/bcf576df9c38b05f/2_vert_update.png",
edgeVertical: "https://static.cdn-luma.com/files/bcf576df9c38b05f/2_hori_update.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 3,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Jitter%20Exported%20Poster.mp4",
defaultPos: { x: 8, y: 0, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/3d36d1e0dba2476c/3_Corner_update.png",
edgeHorizontal: "https://static.cdn-luma.com/files/3d36d1e0dba2476c/3_hori_update.png",
edgeVertical: "https://static.cdn-luma.com/files/3d36d1e0dba2476c/3_Vert_update.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 4,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Exported%20Web%20Video.mp4",
defaultPos: { x: 0, y: 4, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/9e67e05f37e52522/4_corner_update.png",
edgeHorizontal: "https://static.cdn-luma.com/files/9e67e05f37e52522/4_hori_update.png",
edgeVertical: "https://static.cdn-luma.com/files/9e67e05f37e52522/4_vert_update.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 5,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Logo%20Exported.mp4",
defaultPos: { x: 4, y: 4, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/9e67e05f37e52522/5_corner_update.png",
edgeHorizontal: "https://static.cdn-luma.com/files/9e67e05f37e52522/5_hori_update.png",
edgeVertical: "https://static.cdn-luma.com/files/9e67e05f37e52522/5_verti_update.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 6,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Animation%20Exported%20(4).mp4",
defaultPos: { x: 8, y: 4, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/1199340587e8da1d/6_corner.png",
edgeHorizontal: "https://static.cdn-luma.com/files/1199340587e8da1d/6_corner-1.png",
edgeVertical: "https://static.cdn-luma.com/files/1199340587e8da1d/6_vert.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 7,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Illustration%20Exported%20(1).mp4",
defaultPos: { x: 0, y: 8, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/b80b5aa00ccc33bd/7_corner.png",
edgeHorizontal: "https://static.cdn-luma.com/files/b80b5aa00ccc33bd/7_hori.png",
edgeVertical: "https://static.cdn-luma.com/files/b80b5aa00ccc33bd/7_vert.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 8,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Art%20Direction%20Exported.mp4",
defaultPos: { x: 4, y: 8, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/981e483f71aa764b/8_corner.png",
edgeHorizontal: "https://static.cdn-luma.com/files/981e483f71aa764b/8_hori.png",
edgeVertical: "https://static.cdn-luma.com/files/981e483f71aa764b/8_verticle.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
{
id: 9,
video: "https://static.cdn-luma.com/files/58ab7363888153e3/Product%20Video.mp4",
defaultPos: { x: 8, y: 8, w: 4, h: 4 },
corner: "https://static.cdn-luma.com/files/981e483f71aa764b/9_corner.png",
edgeHorizontal: "https://static.cdn-luma.com/files/981e483f71aa764b/9_hori.png",
edgeVertical: "https://static.cdn-luma.com/files/981e483f71aa764b/9_vert.png",
mediaSize: 1,
borderThickness: 0,
borderSize: 80,
autoplayMode: "all",
isHovered: false,
},
]
export default function DynamicFrameLayout() {
const [frames, setFrames] = useState<Frame[]>(initialFrames)
const [hovered, setHovered] = useState<{ row: number; col: number } | null>(null)
const [hoverSize, setHoverSize] = useState(6)
const [gapSize, setGapSize] = useState(4)
const [showControls, setShowControls] = useState(false)
const [cleanInterface, setCleanInterface] = useState(true)
const [showFrames, setShowFrames] = useState(false) // Update: showFrames starts as false
const [autoplayMode, setAutoplayMode] = useState<"all" | "hover">("all")
const getRowSizes = () => {
if (hovered === null) {
return "4fr 4fr 4fr"
}
const { row } = hovered
const nonHoveredSize = (12 - hoverSize) / 2
return [0, 1, 2].map((r) => (r === row ? `${hoverSize}fr` : `${nonHoveredSize}fr`)).join(" ")
}
const getColSizes = () => {
if (hovered === null) {
return "4fr 4fr 4fr"
}
const { col } = hovered
const nonHoveredSize = (12 - hoverSize) / 2
return [0, 1, 2].map((c) => (c === col ? `${hoverSize}fr` : `${nonHoveredSize}fr`)).join(" ")
}
const getTransformOrigin = (x: number, y: number) => {
const vertical = y === 0 ? "top" : y === 4 ? "center" : "bottom"
const horizontal = x === 0 ? "left" : x === 4 ? "center" : "right"
return `${vertical} ${horizontal}`
}
const updateFrameProperty = (id: number, property: keyof Frame, value: number) => {
setFrames(frames.map((frame) => (frame.id === id ? { ...frame, [property]: value } : frame)))
}
const toggleControls = () => {
setShowControls(!showControls)
}
const toggleCleanInterface = () => {
setCleanInterface(!cleanInterface)
if (!cleanInterface) {
setShowControls(false)
}
}
const updateCodebase = () => {
console.log("Updating codebase with current values:")
console.log("Hover Size:", hoverSize)
console.log("Gap Size:", gapSize)
console.log("Frames:", frames)
// Here you would typically make an API call to update the codebase
// For now, we'll just log the values
}
return (
<div className="space-y-4 w-full h-full">
<div className="flex justify-between items-center mb-4">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<Switch id="frame-toggle" checked={showFrames} onCheckedChange={setShowFrames} />
<label htmlFor="frame-toggle" className="text-sm text-white/70">
{showFrames ? "Hide Frames" : "Show Frames"}
</label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="autoplay-toggle"
checked={autoplayMode === "all"}
onCheckedChange={(checked) => setAutoplayMode(checked ? "all" : "hover")}
/>
<label htmlFor="autoplay-toggle" className="text-sm text-white/70">
{autoplayMode === "all" ? "Autoplay All" : "Hover Autoplay"}
</label>
</div>
</div>
</div>
{!cleanInterface && (
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold text-white">Dynamic Frame Layout</h2>
<div className="space-x-2">
<Button onClick={toggleControls}>{showControls ? "Hide Controls" : "Show Controls"}</Button>
<Button onClick={updateCodebase}>Update Codebase</Button>
<Button onClick={toggleCleanInterface}>{cleanInterface ? "Show UI" : "Hide UI"}</Button>
</div>
</div>
)}
{!cleanInterface && showControls && (
<>
<div className="space-y-2">
<label htmlFor="hover-size" className="block text-sm font-medium text-gray-200">
Hover Size: {hoverSize}
</label>
<Slider
id="hover-size"
min={4}
max={8}
step={0.1}
value={[hoverSize]}
onValueChange={(value) => setHoverSize(value[0])}
/>
</div>
<div className="space-y-2">
<label htmlFor="gap-size" className="block text-sm font-medium text-gray-200">
Gap Size: {gapSize}px
</label>
<Slider
id="gap-size"
min={0}
max={20}
step={1}
value={[gapSize]}
onValueChange={(value) => setGapSize(value[0])}
/>
</div>
</>
)}
<div
className="relative w-full h-full"
style={{
display: "grid",
gridTemplateRows: getRowSizes(),
gridTemplateColumns: getColSizes(),
gap: `${gapSize}px`,
transition: "grid-template-rows 0.4s ease, grid-template-columns 0.4s ease",
}}
>
{frames.map((frame) => {
const row = Math.floor(frame.defaultPos.y / 4)
const col = Math.floor(frame.defaultPos.x / 4)
const transformOrigin = getTransformOrigin(frame.defaultPos.x, frame.defaultPos.y)
return (
<motion.div
key={frame.id}
className="relative"
style={{
transformOrigin,
transition: "transform 0.4s ease",
}}
onMouseEnter={() => setHovered({ row, col })}
onMouseLeave={() => setHovered(null)}
>
<FrameComponent
video={frame.video}
width="100%"
height="100%"
className="absolute inset-0"
corner={frame.corner}
edgeHorizontal={frame.edgeHorizontal}
edgeVertical={frame.edgeVertical}
mediaSize={frame.mediaSize}
borderThickness={frame.borderThickness}
borderSize={frame.borderSize}
onMediaSizeChange={(value) => updateFrameProperty(frame.id, "mediaSize", value)}
onBorderThicknessChange={(value) => updateFrameProperty(frame.id, "borderThickness", value)}
onBorderSizeChange={(value) => updateFrameProperty(frame.id, "borderSize", value)}
showControls={showControls && !cleanInterface}
label={`Frame ${frame.id}`}
showFrame={showFrames}
autoplayMode={autoplayMode}
isHovered={
hovered?.row === Math.floor(frame.defaultPos.y / 4) &&
hovered?.col === Math.floor(frame.defaultPos.x / 4)
}
/>
</motion.div>
)
})}
</div>
</div>
)
}