Spaces:
Runtime error
Runtime error
File size: 4,688 Bytes
4782147 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | "use client";
import { ToolUIPart } from "ai";
import equal from "lib/equal";
import { cn } from "lib/utils";
import { ImagesIcon } from "lucide-react";
import { memo, useMemo } from "react";
import { TextShimmer } from "ui/text-shimmer";
import LetterGlitch from "ui/letter-glitch";
interface ImageGeneratorToolInvocationProps {
part: ToolUIPart;
}
interface ImageGenerationResult {
images: {
url: string;
mimeType?: string;
}[];
mode?: "create" | "edit" | "composite";
model: string;
}
function PureImageGeneratorToolInvocation({
part,
}: ImageGeneratorToolInvocationProps) {
const isGenerating = useMemo(() => {
return !part.state.startsWith("output");
}, [part.state]);
const result = useMemo(() => {
if (!part.state.startsWith("output")) return null;
return part.output as ImageGenerationResult;
}, [part.state, part.output]);
const images = useMemo(() => {
return result?.images || [];
}, [result]);
const mode = useMemo(() => {
return result?.mode || "create";
}, [result]);
const hasError = useMemo(() => {
return (
part.state === "output-error" ||
(part.state === "output-available" && result?.images.length === 0)
);
}, [part.state, result]);
// Get mode-specific text
const getModeText = (mode: string) => {
switch (mode) {
case "edit":
return "Editing image...";
case "composite":
return "Compositing images...";
default:
return "Generating image...";
}
};
const getModeHeader = (mode: string) => {
switch (mode) {
case "edit":
return "Image edited";
case "composite":
return "Images composited";
default:
return "Image generated";
}
};
// Simple loading state like web-search
if (isGenerating) {
return (
<div className="flex flex-col gap-4">
<TextShimmer>{getModeText(mode)}</TextShimmer>
<div className="w-full h-96 overflow-hidden rounded-lg">
<LetterGlitch />
</div>
<p className="text-xs text-muted-foreground text-center">
Image generation may take up to 1 minute.
</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
{!hasError && <ImagesIcon className="size-4" />}
<span className="text-sm font-semibold">
{hasError ? "Image generation failed" : getModeHeader(mode)}
</span>
<span className="text-xs text-muted-foreground">{result?.model}</span>
</div>
<div className="w-full flex flex-col gap-3 pb-2">
{hasError ? (
<div className="bg-card text-muted-foreground p-6 rounded-lg text-xs border border-border/20">
{part.errorText ??
(result?.images.length === 0
? "No images generated"
: "Failed to generate image. Please try again.")}
</div>
) : (
<>
<div
className={cn(
"grid gap-3",
images.length === 1
? "grid-cols-1 max-w-2xl"
: "grid-cols-1 md:grid-cols-2 max-w-3xl",
)}
>
{images.map((image, index) => (
<div
key={index}
className="relative group rounded-lg overflow-hidden border border-border hover:border-primary transition-all shadow-sm hover:shadow-md"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={image.url}
loading="lazy"
alt={`Generated image ${index + 1}`}
className="w-full h-auto object-cover"
/>
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors flex items-center justify-center opacity-0 group-hover:opacity-100">
<a
href={image.url}
target="_blank"
rel="noopener noreferrer"
className="bg-primary text-primary-foreground px-4 py-2 rounded-full text-sm font-medium hover:scale-105 transition-transform"
>
Open
</a>
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
);
}
export const ImageGeneratorToolInvocation = memo(
PureImageGeneratorToolInvocation,
(prev, next) => {
return equal(prev.part, next.part);
},
);
|