This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Sparkles, ArrowRight, X, Minus, Database, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
||||
import { useUIStore } from "@/lib/store/uiStore";
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
|
||||
// ── Renderers ──
|
||||
import MarkdownRenderer from "./MarkdownRenderer";
|
||||
import EnergySavingsCalculator from "./EnergySavingsCalculator";
|
||||
import ProcessComparisonTable from "./ProcessComparisonTable";
|
||||
import RFTechExplainer from "./RFTechExplainer";
|
||||
import ConsultationScheduler from "./ConsultationScheduler";
|
||||
import CaseStudyViewer from "./CaseStudyViewer";
|
||||
import EquipmentConfigurator from "./EquipmentConfigurator";
|
||||
import EfficiencyCard from "./EfficiencyCard";
|
||||
|
||||
export default function SilentObserver() {
|
||||
const {
|
||||
isAiExpanded, toggleAi, setAiExpanded,
|
||||
currentSection, activeApplicationTab, setActiveApplicationTab,
|
||||
setHighlightedMapNode, setSelectedMarkerId,
|
||||
} = useUIStore();
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
const [isWideMode, setIsWideMode] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Refs for dynamic body (accessed inside transport function)
|
||||
const sectionRef = useRef(currentSection);
|
||||
const tabRef = useRef(activeApplicationTab);
|
||||
useEffect(() => { sectionRef.current = currentSection; }, [currentSection]);
|
||||
useEffect(() => { tabRef.current = activeApplicationTab; }, [activeApplicationTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkTheme = () => setIsDark(document.documentElement.classList.contains("dark"));
|
||||
checkTheme();
|
||||
const observer = new MutationObserver(checkTheme);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setAiExpanded(false);
|
||||
setTimeout(() => setIsWideMode(false), 400);
|
||||
};
|
||||
|
||||
// ═══ AI SDK 6: Transport with dynamic body ═══
|
||||
const transport = useMemo(() => new DefaultChatTransport({
|
||||
api: "/api/chat",
|
||||
body: () => ({
|
||||
context: {
|
||||
section: sectionRef.current,
|
||||
activeTab: tabRef.current,
|
||||
},
|
||||
}),
|
||||
}), []);
|
||||
|
||||
// ═══ AI SDK 6: useChat ═══
|
||||
const { messages, sendMessage, addToolOutput, status } = useChat({
|
||||
transport,
|
||||
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
||||
|
||||
async onToolCall({ toolCall }) {
|
||||
if (toolCall.dynamic) return;
|
||||
|
||||
if (toolCall.toolName === "navigate_to_section") {
|
||||
const { section, subAction, tabId, nodeId } = toolCall.input as {
|
||||
section: string; subAction?: string; tabId?: string; nodeId?: string;
|
||||
};
|
||||
handleClose();
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(section);
|
||||
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
if (subAction === "activate-tab" && tabId) setActiveApplicationTab(tabId);
|
||||
if (subAction === "highlight-node" && nodeId) {
|
||||
setHighlightedMapNode(nodeId);
|
||||
setTimeout(() => setHighlightedMapNode(null), 5000);
|
||||
}
|
||||
}, 400);
|
||||
addToolOutput({
|
||||
tool: "navigate_to_section" as any,
|
||||
toolCallId: toolCall.toolCallId,
|
||||
output: `Navigated to "${section}" section${tabId ? `, activated tab "${tabId}"` : ""}${nodeId ? `, highlighted node "${nodeId}"` : ""}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = status === "submitted" || status === "streaming";
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}, [messages]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (!isAiExpanded) setAiExpanded(true);
|
||||
setTimeout(() => {
|
||||
sendMessage({
|
||||
text: `I'd like to schedule an engineering consultation${detail?.industry ? ` for my ${detail.industry} operation` : ""}${detail?.installation ? ` (reference: ${detail.installation})` : ""}. Please set that up.`,
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
window.addEventListener("flux:request-consultation", handler);
|
||||
return () => window.removeEventListener("flux:request-consultation", handler);
|
||||
}, [isAiExpanded, setAiExpanded, sendMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
handleClose();
|
||||
setTimeout(() => {
|
||||
const section = document.getElementById("global");
|
||||
if (section) section.scrollIntoView({ behavior: "smooth" });
|
||||
if (detail?.nodeId) {
|
||||
setSelectedMarkerId(detail.nodeId);
|
||||
setHighlightedMapNode(detail.nodeId);
|
||||
setTimeout(() => setHighlightedMapNode(null), 5000);
|
||||
}
|
||||
}, 400);
|
||||
};
|
||||
window.addEventListener("flux:navigate-to-case", handler);
|
||||
return () => window.removeEventListener("flux:navigate-to-case", handler);
|
||||
}, [setSelectedMarkerId, setHighlightedMapNode]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (!detail?.nodeId) return;
|
||||
handleClose();
|
||||
setTimeout(() => {
|
||||
const section = document.getElementById("global");
|
||||
if (section) section.scrollIntoView({ behavior: "smooth" });
|
||||
setSelectedMarkerId(detail.nodeId);
|
||||
}, 400);
|
||||
};
|
||||
window.addEventListener("flux:open-case-study-modal", handler);
|
||||
return () => window.removeEventListener("flux:open-case-study-modal", handler);
|
||||
}, [setSelectedMarkerId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (!isAiExpanded) setAiExpanded(true);
|
||||
setTimeout(() => {
|
||||
if (detail?.prompt) sendMessage({ text: detail.prompt });
|
||||
}, 500);
|
||||
};
|
||||
window.addEventListener("flux:trigger-ai", handler);
|
||||
return () => window.removeEventListener("flux:trigger-ai", handler);
|
||||
}, [isAiExpanded, setAiExpanded, sendMessage]);
|
||||
|
||||
const onSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
};
|
||||
|
||||
function renderToolPart(part: any, index: number) {
|
||||
const key = `tool-${index}`;
|
||||
const ToolLoading = ({ label }: { label: string }) => (<div key={key} className="self-start flex items-center gap-2.5 py-2"><div className="w-5 h-5 rounded-full bg-[#0066CC]/10 dark:bg-[#4DA6FF]/15 flex items-center justify-center"><Sparkles size={10} className="text-[#0066CC] dark:text-[#4DA6FF] animate-pulse" /></div><span className="text-[11px] text-[#86868B] dark:text-[#A1A1A6] animate-pulse">{label}</span></div>);
|
||||
const DataToolWorking = ({ label }: { label: string }) => (<div key={key} className="self-start flex items-center gap-2 py-1"><Database size={10} className="text-[#0066CC]/40 dark:text-[#4DA6FF]/40 animate-pulse" /><span className="text-[10px] text-[#86868B]/60 dark:text-[#A1A1A6]/40 italic">{label}</span></div>);
|
||||
const ToolError = ({ msg }: { msg?: string }) => (<div key={key} className="text-[11px] text-red-400 dark:text-red-300/80 self-start py-1">Analysis unavailable. {msg}</div>);
|
||||
|
||||
if (part.type === "tool-search_installations") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <DataToolWorking key={key} label="Searching installation database..." />;
|
||||
if (part.state === "output-available") return null;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-get_application_knowledge") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <DataToolWorking key={key} label="Loading technical knowledge..." />;
|
||||
if (part.state === "output-available") return null;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-show_case_study") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label="Loading case study..." />;
|
||||
if (part.state === "output-available") { const o = (part as any).output; if (!o?.found) return null; return <CaseStudyViewer key={key} data={o} />; }
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-show_equipment_specs") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label="Loading equipment specifications..." />;
|
||||
if (part.state === "output-available") { const o = (part as any).output; if (!o?.found) return null; return <EquipmentConfigurator key={key} data={o} />; }
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-energy_savings_calculator") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label={`Calculating savings for ${(part as any).input?.industry || "your industry"}...`} />;
|
||||
if (part.state === "output-available") return <EnergySavingsCalculator key={key} data={(part as any).output} />;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-process_comparison_table") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label="Building technology comparison..." />;
|
||||
if (part.state === "output-available") return <ProcessComparisonTable key={key} data={(part as any).output} />;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-rf_technology_explainer") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label="Preparing RF visualization..." />;
|
||||
if (part.state === "output-available") return <RFTechExplainer key={key} data={(part as any).output} />;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-schedule_consultation") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <ToolLoading key={key} label="Preparing your consultation brief..." />;
|
||||
if (part.state === "output-available") return <ConsultationScheduler key={key} data={(part as any).output} />;
|
||||
if (part.state === "output-error") return <ToolError key={key} msg={(part as any).errorText} />;
|
||||
}
|
||||
if (part.type === "tool-navigate_to_section") {
|
||||
if (part.state === "input-streaming" || part.state === "input-available") return <div key={key} className="self-start flex items-center gap-2 py-1"><div className="w-3 h-3 rounded-full bg-[#0066CC] dark:bg-[#4DA6FF] animate-ping opacity-40" /><span className="text-[11px] text-[#86868B] dark:text-[#A1A1A6]">Navigating...</span></div>;
|
||||
if (part.state === "output-available") return <motion.div key={key} initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="self-start text-[10px] text-[#86868B] dark:text-[#A1A1A6]/60 italic py-1">Navigated to section</motion.div>;
|
||||
}
|
||||
if (part.type === "tool-show_efficiency_calculator") {
|
||||
if (part.state === "input-available" || part.state === "output-available") return <EfficiencyCard key={key} industry={(part as any).input?.industry || "Industrial"} estimatedSavingsPercent={(part as any).input?.estimatedSavingsPercent || 40} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{isAiExpanded && (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.5, ease: "easeInOut" }} onClick={handleClose}
|
||||
className={`fixed inset-0 z-40 backdrop-blur-[4px] transition-colors duration-500 ${isWideMode ? "bg-black/30 dark:bg-black/60" : "bg-black/10 dark:bg-black/40"}`} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={`fixed inset-0 z-50 flex pointer-events-none px-3 pb-3 md:p-6 transition-all duration-700 ease-[cubic-bezier(0.16,1,0.3,1)] ${isWideMode ? "items-center justify-center" : "items-end justify-center md:justify-end"}`} style={{ paddingBottom: 'max(0.75rem, env(safe-area-inset-bottom))' }}>
|
||||
<AnimatePresence mode="wait">
|
||||
{!isAiExpanded ? (
|
||||
<motion.button key="pill" layoutId="flux-ai-shell" onClick={toggleAi}
|
||||
initial={{ opacity: 0, scale: 0.8, filter: "blur(10px)" }} animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, scale: 0.9, filter: "blur(10px)" }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
||||
className="pointer-events-auto flex items-center gap-3 px-5 py-3.5 rounded-full cursor-pointer select-none bg-white/70 dark:bg-[#1D1D1F]/80 backdrop-blur-2xl backdrop-saturate-150 border border-white/60 dark:border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.08)] dark:shadow-[0_8px_32px_rgba(0,0,0,0.5)] hover:shadow-[0_12px_40px_rgba(0,0,0,0.12)] dark:hover:shadow-[0_12px_40px_rgba(0,0,0,0.6)] hover:scale-[1.02] active:scale-[0.98] transition-all duration-300 group">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Sparkles size={18} className="text-[#0066CC] dark:text-[#4DA6FF] relative z-10" />
|
||||
<div className="absolute inset-0 bg-[#0066CC] dark:bg-[#4DA6FF] rounded-full blur-md opacity-20 group-hover:opacity-40 transition-opacity" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[#1D1D1F] dark:text-[#F5F5F7] transition-colors">Ask FluxAI</span>
|
||||
</motion.button>
|
||||
) : (
|
||||
<motion.div layout key="panel" layoutId="flux-ai-shell"
|
||||
initial={{ opacity: 0, scale: 0.92, y: 30, filter: "blur(20px)" }} animate={{ opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }} exit={{ opacity: 0, scale: 0.92, y: 30, filter: "blur(20px)" }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 28 }}
|
||||
className={`pointer-events-auto flex flex-col overflow-hidden bg-white/70 dark:bg-[#1D1D1F]/60 backdrop-blur-[40px] backdrop-saturate-150 border border-white/60 dark:border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.12)] dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.04),0_24px_80px_rgba(0,0,0,0.6)] transition-all duration-500 w-full max-w-[calc(100vw-1.5rem)] h-[70vh] max-h-[600px] rounded-[24px] ${isWideMode ? "md:w-[860px] lg:w-[1000px] md:h-[85vh] md:max-h-[900px] md:rounded-[32px]" : "md:w-[460px] md:h-[640px] md:max-h-[640px] md:rounded-[32px]"}`}>
|
||||
<div className="absolute top-0 left-0 w-full h-24 bg-gradient-to-b from-white/[0.06] to-transparent pointer-events-none rounded-t-[24px] md:rounded-t-[32px] hidden dark:block" />
|
||||
|
||||
<div className="relative z-10 flex items-center justify-between px-5 py-4 border-b border-black/[0.04] dark:border-white/[0.06] bg-white/30 dark:bg-white/[0.02] transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Sparkles size={17} className="text-[#0066CC] dark:text-[#4DA6FF] relative z-10" />
|
||||
{isLoading && (<motion.div animate={{ scale: [1, 1.6, 1], opacity: [0.4, 0, 0.4] }} transition={{ duration: 1.5, repeat: Infinity }} className="absolute inset-0 bg-[#0066CC] dark:bg-[#4DA6FF] rounded-full blur-sm" />)}
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-[14px] font-medium text-[#1D1D1F] dark:text-[#F5F5F7] transition-colors">FluxAI</span>
|
||||
<span className="hidden md:inline text-[11px] text-[#86868B] dark:text-[#A1A1A6] transition-colors">Engineering Advisor</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{messages.length > 0 && (
|
||||
<button onClick={() => setIsWideMode(!isWideMode)} className="hidden md:flex p-2 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors group" title={isWideMode ? "Standard View" : "Immersive Focus Mode"}>
|
||||
{isWideMode ? <Minimize2 size={15} className="text-[#86868B] dark:text-[#A1A1A6] group-hover:text-[#1D1D1F] dark:group-hover:text-white transition-colors" /> : <Maximize2 size={15} className="text-[#86868B] dark:text-[#A1A1A6] group-hover:text-[#1D1D1F] dark:group-hover:text-white transition-colors" />}
|
||||
</button>
|
||||
)}
|
||||
<div className="w-px h-4 bg-black/10 dark:bg-white/10 mx-1 hidden md:block" />
|
||||
<button onClick={handleClose} className="p-2 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors group"><Minus size={15} className="text-[#86868B] dark:text-[#A1A1A6] group-hover:text-[#1D1D1F] dark:group-hover:text-white transition-colors" /></button>
|
||||
<button onClick={handleClose} className="p-2 rounded-full hover:bg-black/5 dark:hover:bg-white/5 transition-colors group"><X size={15} className="text-[#86868B] dark:text-[#A1A1A6] group-hover:text-red-500 transition-colors" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={scrollRef} className="flex-1 overflow-y-auto px-5 py-4 flex flex-col gap-4 text-[13px] font-light scroll-smooth [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center flex-1 gap-5 py-8">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#0066CC]/10 to-[#00AAFF]/10 dark:from-[#4DA6FF]/15 dark:to-[#00AAFF]/10 flex items-center justify-center border border-[#0066CC]/10 dark:border-[#4DA6FF]/10 transition-colors"><Sparkles size={24} className="text-[#0066CC] dark:text-[#4DA6FF]" /></div>
|
||||
<div className="text-center">
|
||||
<p className="text-[15px] font-medium text-[#1D1D1F] dark:text-[#F5F5F7] mb-1.5 transition-colors">FluxAI Engineering Advisor</p>
|
||||
<p className="text-[12px] text-[#86868B] dark:text-[#A1A1A6] max-w-[280px] leading-relaxed transition-colors">Ask about energy savings, compare RF vs traditional methods, or let me guide you through our technology.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 justify-center mt-1">
|
||||
{["How much energy can I save?", "RF vs Hot Air", "How does RF heating work?", "Show me proven installations"].map((q) => (
|
||||
<button key={q} onClick={() => sendMessage({ text: q })} className="px-3.5 py-2 rounded-full text-[11px] font-medium bg-white/60 dark:bg-white/[0.06] border border-black/[0.06] dark:border-white/[0.08] text-[#1D1D1F] dark:text-[#A1A1A6] hover:bg-black/5 dark:hover:bg-white/10 hover:text-[#0066CC] dark:hover:text-[#4DA6FF] transition-all duration-200">{q}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((m) => (
|
||||
<div key={m.id} className="flex flex-col gap-2 min-w-0">
|
||||
{m.parts?.map((part, index) => {
|
||||
if (part.type === "text" && part.text) {
|
||||
if (m.role === "user") return <div key={index} className="max-w-[80%] md:max-w-[700px] p-3.5 rounded-2xl rounded-tr-md self-end bg-[#0066CC] dark:bg-[#0066CC]/90 text-white text-[13px] leading-relaxed shadow-md break-words">{part.text}</div>;
|
||||
return <div key={index} className="max-w-[88%] md:max-w-[700px] p-4 rounded-2xl rounded-tl-md self-start bg-white/60 dark:bg-white/[0.05] border border-white/70 dark:border-white/[0.06] text-[#1D1D1F] dark:text-[#E5E5EA] shadow-sm dark:shadow-none transition-colors duration-300 break-words"><MarkdownRenderer content={part.text} /></div>;
|
||||
}
|
||||
if (part.type?.startsWith("tool-")) return renderToolPart(part, index);
|
||||
if (part.type === "step-start" && index > 0) return <div key={index} className="border-t border-black/[0.04] dark:border-white/[0.04] my-1" />;
|
||||
return null;
|
||||
})}
|
||||
|
||||
</div>
|
||||
))}
|
||||
{isLoading && messages.length > 0 && (
|
||||
<div className="self-start flex items-center gap-1.5 py-2 px-1">
|
||||
{[0, 1, 2].map((i) => (<motion.div key={i} className="w-1.5 h-1.5 rounded-full bg-[#0066CC] dark:bg-[#4DA6FF]" animate={{ opacity: [0.2, 1, 0.2], scale: [0.8, 1, 0.8] }} transition={{ duration: 1, repeat: Infinity, delay: i * 0.2 }} />))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={onSubmit} className="relative z-10 px-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] md:pb-3 border-t border-black/[0.04] dark:border-white/[0.06] bg-white/40 dark:bg-white/[0.02] flex items-center gap-2.5 transition-colors duration-300">
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Ask about energy savings, RF technology..." className="flex-1 rounded-2xl border-none outline-none text-[13px] px-4 py-3 bg-black/[0.04] dark:bg-white/[0.06] text-[#1D1D1F] dark:text-[#F5F5F7] placeholder:text-[#86868B] dark:placeholder:text-[#A1A1A6]/60 focus:bg-black/[0.06] dark:focus:bg-white/[0.08] transition-colors duration-200" />
|
||||
<button type="submit" disabled={!input.trim() || isLoading} className="w-10 h-10 rounded-full flex items-center justify-center shrink-0 bg-[#0066CC] dark:bg-[#4DA6FF] text-white dark:text-[#0A0A0C] disabled:opacity-30 hover:shadow-lg hover:scale-105 active:scale-95 shadow-[0_4px_12px_rgba(0,102,204,0.3)] dark:shadow-[0_4px_12px_rgba(77,166,255,0.3)] transition-all duration-200"><ArrowRight size={16} strokeWidth={2.5} /></button>
|
||||
</form>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user