Files
MyClub/ai/animated-ai-input.tsx
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

322 lines
18 KiB
TypeScript

"use client";
import { ArrowRight, Bot, Check, ChevronDown, Paperclip } from "lucide-react";
import { useState, useRef, useCallback, useEffect } from "react";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { motion, AnimatePresence } from "framer-motion";
interface UseAutoResizeTextareaProps {
minHeight: number;
maxHeight?: number;
}
function useAutoResizeTextarea({
minHeight,
maxHeight,
}: UseAutoResizeTextareaProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const adjustHeight = useCallback(
(reset?: boolean) => {
const textarea = textareaRef.current;
if (!textarea) return;
if (reset) {
textarea.style.height = `${minHeight}px`;
return;
}
textarea.style.height = `${minHeight}px`;
const newHeight = Math.max(
minHeight,
Math.min(
textarea.scrollHeight,
maxHeight ?? Number.POSITIVE_INFINITY
)
);
textarea.style.height = `${newHeight}px`;
},
[minHeight, maxHeight]
);
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = `${minHeight}px`;
}
}, [minHeight]);
useEffect(() => {
const handleResize = () => adjustHeight();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, [adjustHeight]);
return { textareaRef, adjustHeight };
}
const OPENAI_ICON = (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 256 260"
aria-label="OpenAI Icon"
className="w-4 h-4 dark:hidden block"
>
<title>OpenAI Icon Light</title>
<path d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z" />
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 256 260"
aria-label="OpenAI Icon"
className="w-4 h-4 hidden dark:block"
>
<title>OpenAI Icon Dark</title>
<path
fill="#fff"
d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"
/>
</svg>
</>
);
export function AI_Prompt() {
const [value, setValue] = useState("");
const { textareaRef, adjustHeight } = useAutoResizeTextarea({
minHeight: 72,
maxHeight: 300,
});
const [selectedModel, setSelectedModel] = useState("GPT-4-1 Mini");
const AI_MODELS = [
"o3-mini",
"Gemini 2.5 Flash",
"Claude 3.5 Sonnet",
"GPT-4-1 Mini",
"GPT-4-1",
];
const MODEL_ICONS: Record<string, React.ReactNode> = {
"o3-mini": OPENAI_ICON,
"Gemini 2.5 Flash": (
<svg
height="1em"
className="w-4 h-4"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Gemini</title>
<defs>
<linearGradient
id="lobe-icons-gemini-fill"
x1="0%"
x2="68.73%"
y1="100%"
y2="30.395%"
>
<stop offset="0%" stopColor="#1C7DFF" />
<stop offset="52.021%" stopColor="#1C69FF" />
<stop offset="100%" stopColor="#F0DCD6" />
</linearGradient>
</defs>
<path
d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12"
fill="url(#lobe-icons-gemini-fill)"
fillRule="nonzero"
/>
</svg>
),
"Claude 3.5 Sonnet": (
<>
<svg
fill="#000"
fillRule="evenodd"
className="w-4 h-4 dark:hidden block"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic Icon Light</title>
<path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" />
</svg>
<svg
fill="#fff"
fillRule="evenodd"
className="w-4 h-4 hidden dark:block"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic Icon Dark</title>
<path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z" />
</svg>
</>
),
"GPT-4-1 Mini": OPENAI_ICON,
"GPT-4-1": OPENAI_ICON,
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey && value.trim()) {
e.preventDefault();
setValue("");
adjustHeight(true);
// Here you can add message sending
}
};
return (
<div className="w-4/6 py-4">
<div className="bg-black/5 dark:bg-white/5 rounded-2xl p-1.5">
<div className="relative">
<div className="relative flex flex-col">
<div
className="overflow-y-auto"
style={{ maxHeight: "400px" }}
>
<Textarea
id="ai-input-15"
value={value}
placeholder={"What can I do for you?"}
className={cn(
"w-full rounded-xl rounded-b-none px-4 py-3 bg-black/5 dark:bg-white/5 border-none dark:text-white placeholder:text-black/70 dark:placeholder:text-white/70 resize-none focus-visible:ring-0 focus-visible:ring-offset-0",
"min-h-[72px]"
)}
ref={textareaRef}
onKeyDown={handleKeyDown}
onChange={(e) => {
setValue(e.target.value);
adjustHeight();
}}
/>
</div>
<div className="h-14 bg-black/5 dark:bg-white/5 rounded-b-xl flex items-center">
<div className="absolute left-3 right-3 bottom-3 flex items-center justify-between w-[calc(100%-24px)]">
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex items-center gap-1 h-8 pl-1 pr-2 text-xs rounded-md dark:text-white hover:bg-black/10 dark:hover:bg-white/10 focus-visible:ring-1 focus-visible:ring-offset-0 focus-visible:ring-blue-500"
>
<AnimatePresence mode="wait">
<motion.div
key={selectedModel}
initial={{
opacity: 0,
y: -5,
}}
animate={{
opacity: 1,
y: 0,
}}
exit={{
opacity: 0,
y: 5,
}}
transition={{
duration: 0.15,
}}
className="flex items-center gap-1"
>
{
MODEL_ICONS[
selectedModel
]
}
{selectedModel}
<ChevronDown className="w-3 h-3 opacity-50" />
</motion.div>
</AnimatePresence>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className={cn(
"min-w-[10rem]",
"border-black/10 dark:border-white/10",
"bg-gradient-to-b from-white via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-800"
)}
>
{AI_MODELS.map((model) => (
<DropdownMenuItem
key={model}
onSelect={() =>
setSelectedModel(model)
}
className="flex items-center justify-between gap-2"
>
<div className="flex items-center gap-2">
{MODEL_ICONS[model] || (
<Bot className="w-4 h-4 opacity-50" />
)}
<span>{model}</span>
</div>
{selectedModel ===
model && (
<Check className="w-4 h-4 text-blue-500" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<div className="h-4 w-px bg-black/10 dark:bg-white/10 mx-0.5" />
<label
className={cn(
"rounded-lg p-2 bg-black/5 dark:bg-white/5 cursor-pointer",
"hover:bg-black/10 dark:hover:bg-white/10 focus-visible:ring-1 focus-visible:ring-offset-0 focus-visible:ring-blue-500",
"text-black/40 dark:text-white/40 hover:text-black dark:hover:text-white"
)}
aria-label="Attach file"
>
<input type="file" className="hidden" />
<Paperclip className="w-4 h-4 transition-colors" />
</label>
</div>
<button
type="button"
className={cn(
"rounded-lg p-2 bg-black/5 dark:bg-white/5",
"hover:bg-black/10 dark:hover:bg-white/10 focus-visible:ring-1 focus-visible:ring-offset-0 focus-visible:ring-blue-500"
)}
aria-label="Send message"
disabled={!value.trim()}
onClick={() => {
if (!value.trim()) return;
setValue("");
adjustHeight(true);
// Здесь можно добавить отправку сообщения
}}
>
<ArrowRight
className={cn(
"w-4 h-4 dark:text-white transition-opacity duration-200",
value.trim()
? "opacity-100"
: "opacity-30"
)}
/>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}