production: docker + nginx config for rf-flux.com
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-03-20 13:46:05 -05:00
parent b275b19f08
commit fc24313f15
187 changed files with 20977 additions and 767 deletions
+319
View File
@@ -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>
</>
);
}