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>
223 lines
6.8 KiB
TypeScript
223 lines
6.8 KiB
TypeScript
"use client"
|
|
import { Slider } from "@/components/ui/slider"
|
|
import { useEffect, useRef } from "react"
|
|
|
|
interface FrameComponentProps {
|
|
video: string
|
|
width: number | string
|
|
height: number | string
|
|
className?: string
|
|
corner: string
|
|
edgeHorizontal: string
|
|
edgeVertical: string
|
|
mediaSize: number
|
|
borderThickness: number
|
|
borderSize: number
|
|
onMediaSizeChange: (value: number) => void
|
|
onBorderThicknessChange: (value: number) => void
|
|
onBorderSizeChange: (value: number) => void
|
|
showControls: boolean
|
|
label: string
|
|
showFrame: boolean
|
|
autoplayMode: "all" | "hover"
|
|
isHovered: boolean
|
|
}
|
|
|
|
export function FrameComponent({
|
|
video,
|
|
width,
|
|
height,
|
|
className = "",
|
|
corner,
|
|
edgeHorizontal,
|
|
edgeVertical,
|
|
mediaSize,
|
|
borderThickness,
|
|
borderSize,
|
|
onMediaSizeChange,
|
|
onBorderThicknessChange,
|
|
onBorderSizeChange,
|
|
showControls,
|
|
label,
|
|
showFrame,
|
|
autoplayMode,
|
|
isHovered,
|
|
}: FrameComponentProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (autoplayMode === "all") {
|
|
videoRef.current?.play()
|
|
} else if (autoplayMode === "hover") {
|
|
if (isHovered) {
|
|
videoRef.current?.play()
|
|
} else {
|
|
videoRef.current?.pause()
|
|
}
|
|
}
|
|
}, [isHovered, autoplayMode])
|
|
|
|
return (
|
|
<div
|
|
className={`relative ${className}`}
|
|
style={{
|
|
width,
|
|
height,
|
|
transition: "width 0.3s ease-in-out, height 0.3s ease-in-out",
|
|
}}
|
|
>
|
|
<div className="relative w-full h-full overflow-hidden">
|
|
{/* Video with Border */}
|
|
<div
|
|
className="absolute inset-0 flex items-center justify-center"
|
|
style={{
|
|
zIndex: 1,
|
|
transition: "all 0.3s ease-in-out",
|
|
padding: showFrame ? `${borderThickness}px` : "0",
|
|
width: showFrame ? `${borderSize}%` : "100%",
|
|
height: showFrame ? `${borderSize}%` : "100%",
|
|
left: showFrame ? `${(100 - borderSize) / 2}%` : "0",
|
|
top: showFrame ? `${(100 - borderSize) / 2}%` : "0",
|
|
}}
|
|
>
|
|
<div
|
|
className="w-full h-full overflow-hidden"
|
|
style={{
|
|
transform: `scale(${mediaSize})`,
|
|
transformOrigin: "center",
|
|
transition: "transform 0.3s ease-in-out",
|
|
}}
|
|
>
|
|
<video
|
|
className="w-full h-full object-cover"
|
|
src={video}
|
|
loop
|
|
muted
|
|
playsInline
|
|
autoPlay={autoplayMode === "all" || (autoplayMode === "hover" && isHovered)}
|
|
ref={videoRef}
|
|
onMouseEnter={(e) => {
|
|
if (autoplayMode === "hover") {
|
|
e.currentTarget.play()
|
|
}
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (autoplayMode === "hover") {
|
|
e.currentTarget.pause()
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Frame Elements (Higher z-index) */}
|
|
{showFrame && (
|
|
<div className="absolute inset-0" style={{ zIndex: 2 }}>
|
|
{/* Corners */}
|
|
<div
|
|
className="absolute top-0 left-0 w-16 h-16 bg-contain bg-no-repeat"
|
|
style={{ backgroundImage: `url(${corner})` }}
|
|
/>
|
|
<div
|
|
className="absolute top-0 right-0 w-16 h-16 bg-contain bg-no-repeat"
|
|
style={{ backgroundImage: `url(${corner})`, transform: "scaleX(-1)" }}
|
|
/>
|
|
<div
|
|
className="absolute bottom-0 left-0 w-16 h-16 bg-contain bg-no-repeat"
|
|
style={{ backgroundImage: `url(${corner})`, transform: "scaleY(-1)" }}
|
|
/>
|
|
<div
|
|
className="absolute bottom-0 right-0 w-16 h-16 bg-contain bg-no-repeat"
|
|
style={{ backgroundImage: `url(${corner})`, transform: "scale(-1, -1)" }}
|
|
/>
|
|
|
|
{/* Edges */}
|
|
<div
|
|
className="absolute top-0 left-16 right-16 h-16"
|
|
style={{
|
|
backgroundImage: `url(${edgeHorizontal})`,
|
|
backgroundSize: "auto 64px",
|
|
backgroundRepeat: "repeat-x",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute bottom-0 left-16 right-16 h-16"
|
|
style={{
|
|
backgroundImage: `url(${edgeHorizontal})`,
|
|
backgroundSize: "auto 64px",
|
|
backgroundRepeat: "repeat-x",
|
|
transform: "rotate(180deg)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute left-0 top-16 bottom-16 w-16"
|
|
style={{
|
|
backgroundImage: `url(${edgeVertical})`,
|
|
backgroundSize: "64px auto",
|
|
backgroundRepeat: "repeat-y",
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute right-0 top-16 bottom-16 w-16"
|
|
style={{
|
|
backgroundImage: `url(${edgeVertical})`,
|
|
backgroundSize: "64px auto",
|
|
backgroundRepeat: "repeat-y",
|
|
transform: "scaleX(-1)",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
{showControls && (
|
|
<div className="absolute bottom-0 left-0 right-0 p-2 bg-black bg-opacity-50 z-10">
|
|
<div className="text-white font-bold mb-2">{label}</div>
|
|
<div className="space-y-2">
|
|
<div>
|
|
<label htmlFor={`media-size-${label}`} className="block text-sm font-medium text-white">
|
|
Media Size: {mediaSize.toFixed(2)}
|
|
</label>
|
|
<Slider
|
|
id={`media-size-${label}`}
|
|
min={0.5}
|
|
max={3}
|
|
step={0.01}
|
|
value={[mediaSize]}
|
|
onValueChange={(value) => onMediaSizeChange(value[0])}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`border-thickness-${label}`} className="block text-sm font-medium text-white">
|
|
Border Thickness: {borderThickness}px
|
|
</label>
|
|
<Slider
|
|
id={`border-thickness-${label}`}
|
|
min={0}
|
|
max={20}
|
|
step={1}
|
|
value={[borderThickness]}
|
|
onValueChange={(value) => onBorderThicknessChange(value[0])}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor={`border-size-${label}`} className="block text-sm font-medium text-white">
|
|
Border Size: {borderSize}%
|
|
</label>
|
|
<Slider
|
|
id={`border-size-${label}`}
|
|
min={50}
|
|
max={100}
|
|
step={1}
|
|
value={[borderSize]}
|
|
onValueChange={(value) => onBorderSizeChange(value[0])}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|