better-chatbot / src /components /tool-invocation /image-generator.tsx
Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
4.69 kB
"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);
},
);