mirror of
https://github.com/moumen-soliman/uitripled
synced 2026-04-21 13:37:20 +00:00
331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
ArrowUpRight,
|
|
ChevronDown,
|
|
Clock,
|
|
Link2,
|
|
Mail,
|
|
MessageSquare,
|
|
Share2,
|
|
Sparkles,
|
|
Star,
|
|
Zap,
|
|
} from "lucide-react";
|
|
|
|
type DropdownType = "share" | "quick" | "history" | "magic" | "model" | null;
|
|
|
|
type ActionOption = {
|
|
icon: typeof Link2;
|
|
label: string;
|
|
action: string;
|
|
};
|
|
|
|
const shareOptions: ActionOption[] = [
|
|
{ icon: Link2, label: "Copy link", action: "copy-link" },
|
|
{ icon: Mail, label: "Email", action: "email" },
|
|
{ icon: MessageSquare, label: "Slack", action: "slack" },
|
|
];
|
|
|
|
const quickOptions: ActionOption[] = [
|
|
{ icon: Sparkles, label: "Summarize", action: "summarize" },
|
|
{ icon: Sparkles, label: "Improve", action: "improve" },
|
|
{ icon: Sparkles, label: "Translate", action: "translate" },
|
|
];
|
|
|
|
const historyOptions: ActionOption[] = [
|
|
{ icon: Clock, label: "Last hour", action: "history-1h" },
|
|
{ icon: Clock, label: "Today", action: "history-today" },
|
|
{ icon: Clock, label: "This week", action: "history-week" },
|
|
];
|
|
|
|
const magicOptions: ActionOption[] = [
|
|
{ icon: Sparkles, label: "Auto-complete", action: "magic-complete" },
|
|
{ icon: Sparkles, label: "Storyboard", action: "magic-storyboard" },
|
|
{ icon: Sparkles, label: "Rephrase", action: "magic-rephrase" },
|
|
];
|
|
|
|
const models = ["GPT 5.0", "GPT 4.5 Turbo", "GPT 4.0", "Claude 3.5 Sonnet"];
|
|
|
|
export function AIChatInterface() {
|
|
const [inputValue, setInputValue] = useState("");
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
const [activeDropdown, setActiveDropdown] = useState<DropdownType>(null);
|
|
const [selectedModel, setSelectedModel] = useState(models[0]);
|
|
const prefersReducedMotion = useReducedMotion();
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const shouldAnimate = !prefersReducedMotion;
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setActiveDropdown(null);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, []);
|
|
|
|
const handleTextareaChange = (
|
|
event: React.ChangeEvent<HTMLTextAreaElement>
|
|
) => {
|
|
setInputValue(event.target.value);
|
|
|
|
if (textareaRef.current) {
|
|
textareaRef.current.style.height = "auto";
|
|
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
}
|
|
};
|
|
|
|
const renderDropdown = (
|
|
type: DropdownType,
|
|
options: ActionOption[],
|
|
align: "left" | "right" = "left"
|
|
) => (
|
|
<AnimatePresence>
|
|
{activeDropdown === type && (
|
|
<motion.div
|
|
key={type}
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -8 }}
|
|
transition={{ duration: 0.18, ease: "easeOut" }}
|
|
className={`absolute ${align === "right" ? "right-0" : "left-0"} mt-3 w-56 rounded-2xl border border-border/40 bg-background/85 py-2 shadow-[0_24px_60px_rgba(15,23,42,0.24)] backdrop-blur-xl`}
|
|
role="menu"
|
|
>
|
|
{options.map((option) => (
|
|
<Button
|
|
key={option.action}
|
|
onClick={() => {
|
|
console.log(`Action: ${option.action}`);
|
|
setActiveDropdown(null);
|
|
}}
|
|
className="flex w-full justify-start items-center gap-3 px-4 py-2 text-sm text-foreground/85 transition-all hover:bg-foreground/[0.05] hover:text-foreground"
|
|
role="menuitem"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<option.icon size={16} className="text-foreground/60" />
|
|
<span>{option.label}</span>
|
|
</Button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
|
|
return (
|
|
<motion.section
|
|
initial={shouldAnimate ? { opacity: 0, y: 20 } : { opacity: 1, y: 0 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={shouldAnimate ? { duration: 0.5 } : { duration: 0 }}
|
|
className="relative w-full"
|
|
>
|
|
<div className="space-y-8">
|
|
<div
|
|
className={`relative rounded-[28px] border border-border/50 bg-background/70 px-6 py-6 backdrop-blur-xl transition w-full z-10 ${
|
|
isFocused ? "shadow-[0_28px_80px_rgba(15,23,42,0.24)]" : ""
|
|
}`}
|
|
>
|
|
<div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/20/20 via-transparent to-transparent" />
|
|
<div className="relative space-y-5">
|
|
<div className="flex flex-col gap-1 text-left">
|
|
<span className="text-xs font-semibold uppercase tracking-[0.32em] text-foreground/45">
|
|
Prompt
|
|
</span>
|
|
<p className="text-sm text-foreground/60">
|
|
Share goals, context, tone, and desired output.
|
|
</p>
|
|
</div>
|
|
<label htmlFor="chat-input" className="sr-only">
|
|
Ask AI anything
|
|
</label>
|
|
<Textarea
|
|
id="chat-input"
|
|
ref={textareaRef}
|
|
value={inputValue}
|
|
onChange={handleTextareaChange}
|
|
placeholder="Ask AI anything..."
|
|
className="w-full resize-none bg-transparent text-base text-foreground/90 placeholder:text-foreground/40 focus:outline-none"
|
|
onFocus={() => setIsFocused(true)}
|
|
onBlur={() => setIsFocused(false)}
|
|
rows={1}
|
|
aria-label="Chat input"
|
|
/>
|
|
|
|
<div
|
|
className="flex flex-wrap items-center gap-2 border-t border-border/25 pt-3"
|
|
ref={dropdownRef}
|
|
>
|
|
<div className="relative">
|
|
<Button
|
|
onClick={() =>
|
|
setActiveDropdown(
|
|
activeDropdown === "share" ? null : "share"
|
|
)
|
|
}
|
|
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
|
|
aria-label="Share options"
|
|
aria-expanded={activeDropdown === "share"}
|
|
aria-haspopup="menu"
|
|
type="button"
|
|
>
|
|
<Share2
|
|
size={18}
|
|
className="text-foreground/50 group-hover:text-foreground"
|
|
strokeWidth={2}
|
|
/>
|
|
</Button>
|
|
{renderDropdown("share", shareOptions)}
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Button
|
|
onClick={() =>
|
|
setActiveDropdown(
|
|
activeDropdown === "quick" ? null : "quick"
|
|
)
|
|
}
|
|
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
|
|
aria-label="Quick actions"
|
|
aria-expanded={activeDropdown === "quick"}
|
|
aria-haspopup="menu"
|
|
type="button"
|
|
>
|
|
<Zap
|
|
size={18}
|
|
className="text-foreground/50 group-hover:text-foreground"
|
|
strokeWidth={2}
|
|
/>
|
|
</Button>
|
|
{renderDropdown("quick", quickOptions)}
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Button
|
|
onClick={() =>
|
|
setActiveDropdown(
|
|
activeDropdown === "history" ? null : "history"
|
|
)
|
|
}
|
|
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
|
|
aria-label="History"
|
|
aria-expanded={activeDropdown === "history"}
|
|
aria-haspopup="menu"
|
|
type="button"
|
|
>
|
|
<Clock
|
|
size={18}
|
|
className="text-foreground/50 group-hover:text-foreground"
|
|
strokeWidth={2}
|
|
/>
|
|
</Button>
|
|
{renderDropdown("history", historyOptions)}
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Button
|
|
onClick={() =>
|
|
setActiveDropdown(
|
|
activeDropdown === "magic" ? null : "magic"
|
|
)
|
|
}
|
|
className="group rounded-xl p-2 bg-background/80 transition-all hover:bg-foreground/[0.05] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
|
|
aria-label="Magic options"
|
|
aria-expanded={activeDropdown === "magic"}
|
|
aria-haspopup="menu"
|
|
type="button"
|
|
>
|
|
<Sparkles
|
|
size={18}
|
|
className="text-foreground/50 group-hover:text-foreground"
|
|
strokeWidth={2}
|
|
/>
|
|
</Button>
|
|
{renderDropdown("magic", magicOptions)}
|
|
</div>
|
|
|
|
<div className="relative ml-auto">
|
|
<Button
|
|
onClick={() =>
|
|
setActiveDropdown(
|
|
activeDropdown === "model" ? null : "model"
|
|
)
|
|
}
|
|
className="flex items-center gap-2 rounded-xl border border-border/30 bg-background/80 px-3 py-1.5 text-sm font-medium text-foreground/80 transition hover:border-border/40 hover:bg-background/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/40"
|
|
aria-label="Select AI model"
|
|
aria-expanded={activeDropdown === "model"}
|
|
aria-haspopup="listbox"
|
|
type="button"
|
|
>
|
|
<span>{selectedModel}</span>
|
|
<ChevronDown size={16} className="text-foreground/50" />
|
|
</Button>
|
|
|
|
<AnimatePresence>
|
|
{activeDropdown === "model" && (
|
|
<motion.div
|
|
key="model-dropdown"
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -8 }}
|
|
transition={{ duration: 0.18, ease: "easeOut" }}
|
|
className="absolute right-0 mt-3 w-56 rounded-2xl border border-border/40 bg-background/85 py-2 shadow-[0_24px_60px_rgba(15,23,42,0.24)] backdrop-blur-xl z-50"
|
|
role="listbox"
|
|
>
|
|
{models.map((model) => (
|
|
<Button
|
|
key={model}
|
|
onClick={() => {
|
|
setSelectedModel(model);
|
|
setActiveDropdown(null);
|
|
}}
|
|
className={`flex w-full items-center justify-between px-4 py-2 text-sm transition-all ${
|
|
selectedModel === model
|
|
? "bg-primary/15 font-medium text-primary hover:bg-primary/20"
|
|
: "text-foreground/80 hover:bg-foreground/[0.05]"
|
|
}`}
|
|
role="option"
|
|
aria-selected={selectedModel === model}
|
|
type="button"
|
|
variant={
|
|
selectedModel === model ? "default" : "ghost"
|
|
}
|
|
>
|
|
{model}
|
|
{selectedModel === model && (
|
|
<Star size={14} className="text-primary/80" />
|
|
)}
|
|
</Button>
|
|
))}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
<Button
|
|
type="button"
|
|
size="lg"
|
|
className="inline-flex items-center gap-2 rounded-full px-5 text-xs uppercase tracking-[0.28em]"
|
|
onClick={() => console.log("Action: send-message")}
|
|
>
|
|
Send
|
|
<ArrowUpRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.section>
|
|
);
|
|
}
|