249 lines
8.8 KiB
TypeScript
249 lines
8.8 KiB
TypeScript
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
import {
|
|
Bot,
|
|
Sparkles,
|
|
FileText,
|
|
Wand2,
|
|
SlidersHorizontal,
|
|
ListChecks,
|
|
MessageSquareText,
|
|
} from "lucide-react";
|
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
import { Terminal, AnimatedStep } from "@/components/ui/terminal";
|
|
import { TypingAnimation } from "@/components/ui/typing-animation";
|
|
import { AnimatedBeam } from "@/components/ui/animated-beam";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const PROMPT_CREATE_STEPS = [
|
|
"Interpretando instruções e contexto...",
|
|
"Definindo objetivo, público e tom...",
|
|
"Organizando estrutura do prompt (seções e regras)...",
|
|
"Especificando formato de saída e restrições...",
|
|
"Adicionando critérios de qualidade e validações...",
|
|
"Incluindo exemplos e casos de borda...",
|
|
"Revisando clareza e consistência...",
|
|
"Finalizando prompt...",
|
|
] as const;
|
|
|
|
const PROMPT_REFINE_STEPS = [
|
|
"Lendo o prompt atual...",
|
|
"Identificando ambiguidades e pontos fracos...",
|
|
"Removendo redundâncias e ruído...",
|
|
"Aprimorando instruções e critérios de sucesso...",
|
|
"Ajustando tom, voz e consistência...",
|
|
"Fortalecendo formato de saída e validações...",
|
|
"Adicionando exemplos e casos de borda...",
|
|
"Consolidando melhorias e finalizando...",
|
|
] as const;
|
|
|
|
const STEP_INTERVAL_MS = 1500;
|
|
|
|
export type PromptGeneratingMode = "create" | "refine";
|
|
|
|
export interface PromptGeneratingScreenProps {
|
|
mode: PromptGeneratingMode;
|
|
progress?: number;
|
|
message?: string;
|
|
}
|
|
|
|
function visibleLineCount(progress: number, stepCount: number): number {
|
|
if (progress <= 0) return 1;
|
|
const phase = (progress / 100) * stepCount;
|
|
const stepInCycle = Math.floor(phase) % stepCount;
|
|
return Math.min(stepInCycle + 1, stepCount);
|
|
}
|
|
|
|
const BeamCircle = forwardRef<
|
|
HTMLDivElement,
|
|
{ className?: string; children?: React.ReactNode }
|
|
>(({ className, children }, ref) => (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
"z-10 flex size-12 items-center justify-center rounded-full border-2 border-slate-300 bg-slate-100 text-slate-700 shadow-sm",
|
|
className
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
));
|
|
BeamCircle.displayName = "BeamCircle";
|
|
|
|
export function PromptGeneratingScreen({
|
|
mode,
|
|
progress,
|
|
message = "Assim que finalizar, o modal será fechado automaticamente.",
|
|
}: PromptGeneratingScreenProps) {
|
|
const steps = mode === "refine" ? PROMPT_REFINE_STEPS : PROMPT_CREATE_STEPS;
|
|
const stepCount = steps.length;
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const sparklesRef = useRef<HTMLDivElement>(null);
|
|
const fileRef = useRef<HTMLDivElement>(null);
|
|
const slidersRef = useRef<HTMLDivElement>(null);
|
|
const centerRef = useRef<HTMLDivElement>(null);
|
|
const wandRef = useRef<HTMLDivElement>(null);
|
|
const checklistRef = useRef<HTMLDivElement>(null);
|
|
const chatRef = useRef<HTMLDivElement>(null);
|
|
|
|
const [loopStep, setLoopStep] = useState(0);
|
|
useEffect(() => {
|
|
if (progress !== undefined) return;
|
|
const id = setInterval(() => {
|
|
setLoopStep((s) => (s + 1) % stepCount);
|
|
}, STEP_INTERVAL_MS);
|
|
return () => clearInterval(id);
|
|
}, [progress, stepCount]);
|
|
|
|
const visibleCount =
|
|
progress !== undefined
|
|
? visibleLineCount(progress, stepCount)
|
|
: loopStep + 1;
|
|
|
|
return (
|
|
<Dialog open>
|
|
<DialogContent
|
|
hideClose
|
|
overlayClassName="bg-black/50"
|
|
className="max-w-2xl gap-0 overflow-hidden bg-white p-0 sm:rounded-xl"
|
|
>
|
|
<div className="flex flex-col gap-4 p-5">
|
|
<div className="flex items-center gap-3 rounded-t-lg border border-b-0 border-border bg-zinc-100 px-3 py-2.5">
|
|
<div className="flex gap-1.5">
|
|
<span className="size-3 rounded-full bg-[#ff5f57]" aria-hidden />
|
|
<span className="size-3 rounded-full bg-[#febc2e]" aria-hidden />
|
|
<span className="size-3 rounded-full bg-[#28c840]" aria-hidden />
|
|
</div>
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
codex prompt — {mode === "refine" ? "melhorando" : "gerando"}
|
|
</span>
|
|
</div>
|
|
|
|
<Terminal className="min-h-[200px] space-y-2 rounded-t-none border-t-0 bg-white p-4 font-mono text-sm">
|
|
<AnimatedStep delay={0} className="text-muted-foreground">
|
|
{`> codex prompt --${mode === "refine" ? "refine" : "create"}`}
|
|
</AnimatedStep>
|
|
{steps.slice(0, visibleCount).map((text, i) => (
|
|
<div key={`${visibleCount}-${i}-${text}`} className="flex items-center gap-0">
|
|
{i < visibleCount - 1 ? (
|
|
<span className="text-sky-600 dark:text-sky-400">
|
|
✔ {text}
|
|
</span>
|
|
) : (
|
|
<TypingAnimation
|
|
key={`typing-${visibleCount}`}
|
|
startOnView={false}
|
|
showCursor
|
|
cursorStyle="block"
|
|
className="text-sky-600 dark:text-sky-400"
|
|
>
|
|
{`✔ ${text}`}
|
|
</TypingAnimation>
|
|
)}
|
|
</div>
|
|
))}
|
|
</Terminal>
|
|
|
|
{message && (
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
{message}
|
|
</p>
|
|
)}
|
|
|
|
<div className="rounded-xl border border-border bg-white p-6">
|
|
<div
|
|
ref={containerRef}
|
|
className="relative flex h-[260px] w-full items-center justify-center overflow-hidden"
|
|
>
|
|
<div className="flex size-full max-h-[220px] max-w-lg flex-col items-stretch justify-between">
|
|
<div className="flex flex-row items-center justify-between">
|
|
<BeamCircle ref={sparklesRef}>
|
|
<Sparkles className="size-6 text-sky-500" />
|
|
</BeamCircle>
|
|
<BeamCircle ref={fileRef}>
|
|
<FileText className="size-6 text-slate-600" />
|
|
</BeamCircle>
|
|
</div>
|
|
<div className="flex flex-row items-center justify-between">
|
|
<BeamCircle ref={slidersRef}>
|
|
<SlidersHorizontal className="size-6 text-indigo-500" />
|
|
</BeamCircle>
|
|
<BeamCircle ref={centerRef} className="size-14 border-slate-400 bg-slate-200">
|
|
<Bot className="size-8 text-slate-700" />
|
|
</BeamCircle>
|
|
<BeamCircle ref={wandRef}>
|
|
<Wand2 className="size-6 text-amber-500" />
|
|
</BeamCircle>
|
|
</div>
|
|
<div className="flex flex-row items-center justify-between">
|
|
<BeamCircle ref={checklistRef}>
|
|
<ListChecks className="size-6 text-slate-600" />
|
|
</BeamCircle>
|
|
<BeamCircle ref={chatRef}>
|
|
<MessageSquareText className="size-6 text-slate-600" />
|
|
</BeamCircle>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={sparklesRef}
|
|
toRef={centerRef}
|
|
curvature={-75}
|
|
endYOffset={-10}
|
|
gradientStartColor="#0ea5e9"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={fileRef}
|
|
toRef={centerRef}
|
|
curvature={-75}
|
|
endYOffset={-10}
|
|
reverse
|
|
gradientStartColor="#94a3b8"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={slidersRef}
|
|
toRef={centerRef}
|
|
gradientStartColor="#6366f1"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={wandRef}
|
|
toRef={centerRef}
|
|
reverse
|
|
gradientStartColor="#f59e0b"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={checklistRef}
|
|
toRef={centerRef}
|
|
curvature={75}
|
|
endYOffset={10}
|
|
gradientStartColor="#94a3b8"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
<AnimatedBeam
|
|
containerRef={containerRef}
|
|
fromRef={chatRef}
|
|
toRef={centerRef}
|
|
curvature={75}
|
|
endYOffset={10}
|
|
reverse
|
|
gradientStartColor="#94a3b8"
|
|
gradientStopColor="#64748b"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|