11"use client" ;
22
3- import { createChat } from "@n8n/chat " ;
3+ import { Button } from "@/components/ui/button " ;
44import config from "@/lib/config" ;
5- import { useEffect } from "react" ;
5+ import { createChat } from "@n8n/chat" ;
6+ import { useEffect , useRef , useState } from "react" ;
67
78import "@n8n/chat/style.css" ;
89import "../styles/ai-agent.css" ;
10+ import { LoaderCircle , SparklesIcon } from "lucide-react" ;
11+ import Image from "next/image" ;
12+ import { cn } from "@/lib/utils" ;
13+ import useMedia from "@/hooks/use-media" ;
14+
15+ const DEFAULT_MESSAGES = [
16+ { id : 1 , text : "What services does Hyperjump offer?" } ,
17+ { id : 2 , text : "Show me examples of past projects" } ,
18+ { id : 3 , text : "Schedule a free consultation" }
19+ ] ;
20+
21+ export default function AIAgent ( ) {
22+ const isDesktop = useMedia ( "(min-width: 992px)" ) ;
23+ const [ text , setText ] = useState < string > ( "" ) ;
24+ const [ isSubmitted , setIsSubmitted ] = useState < boolean > ( false ) ;
25+ const [ isChatOpen , setIsChatOpen ] = useState < boolean > ( true ) ;
26+
27+ // Refs to store DOM elements
28+ const chatDivRef = useRef < HTMLElement | null > ( null ) ;
29+ const chatWindowRef = useRef < HTMLElement | null > ( null ) ;
30+ const chatFABRef = useRef < HTMLElement | null > ( null ) ;
31+
32+ // Helper function to trigger click event
33+ const triggerClick = ( element : Element ) => {
34+ const clickEvent = new MouseEvent ( "click" , {
35+ bubbles : true ,
36+ cancelable : true ,
37+ view : window
38+ } ) ;
39+ element . dispatchEvent ( clickEvent ) ;
40+ } ;
41+
42+ // Helper function to initialize chat elements
43+ const initializeChatElements = ( ) => {
44+ const chatDiv = document . querySelector ( "#n8n-chat" ) ;
45+ if ( ! chatDiv ) return ;
46+
47+ chatDivRef . current = chatDiv as HTMLElement ;
48+ chatWindowRef . current = chatDiv . querySelector (
49+ ".chat-window-wrapper"
50+ ) as HTMLElement ;
51+ chatFABRef . current = chatDiv . querySelector (
52+ ".chat-window-toggle"
53+ ) as HTMLElement ;
954
10- export const AIAgent = ( ) => {
55+ if ( chatWindowRef . current ) {
56+ chatWindowRef . current . classList . toggle ( "chat-window-minimized" ) ;
57+ }
58+ } ;
59+
60+ // Effect to create the chat widget
1161 useEffect ( ( ) => {
1262 if ( config . AI_AGENT_URL ) {
13- createChat ( {
63+ const chat = createChat ( {
1464 webhookUrl : config . AI_AGENT_URL ,
1565 initialMessages : [
1666 `Hi there! 👋` ,
@@ -30,8 +80,249 @@ export const AIAgent = () => {
3080 }
3181 }
3282 } ) ;
83+ initializeChatElements ( ) ;
84+
85+ // Create chat controls
86+ const chatHeader = chatDivRef . current ?. querySelector ( ".chat-header" ) ;
87+ if ( chatHeader ) {
88+ const createButton = (
89+ title : string ,
90+ icon : string ,
91+ marginRight : string ,
92+ onClick : ( ) => void
93+ ) => {
94+ const button = document . createElement ( "button" ) ;
95+ button . classList . add (
96+ "absolute" ,
97+ "flex" ,
98+ "items-center" ,
99+ "justify-center" ,
100+ "right-0" ,
101+ "top-0" ,
102+ "text-white" ,
103+ "border" ,
104+ "border-white" ,
105+ "mt-4" ,
106+ marginRight ,
107+ "h-7" ,
108+ "w-7" ,
109+ "rounded-full" ,
110+ "bg-transparent" ,
111+ "hover:bg-gray-100" ,
112+ "hover:text-black" ,
113+ "transition-all" ,
114+ "duration-300"
115+ ) ;
116+ button . type = "button" ;
117+ button . title = title ;
118+ button . innerHTML = icon ;
119+ button . onclick = onClick ;
120+ return button ;
121+ } ;
122+
123+ // Add minimize button
124+ chatHeader . appendChild (
125+ createButton (
126+ "Minimize" ,
127+ `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minus-icon lucide-minus"><path d="M5 12h14"/></svg>` ,
128+ "mr-4" ,
129+ ( ) => {
130+ if ( chatFABRef . current ) {
131+ triggerClick ( chatFABRef . current ) ;
132+ setIsChatOpen ( false ) ;
133+ if ( ! isDesktop ) {
134+ chatFABRef . current . setAttribute ( "style" , "display:flex;" ) ;
135+ } else if ( chatWindowRef . current ) {
136+ chatWindowRef . current . classList . add ( "chat-window-right" ) ;
137+ chatWindowRef . current . classList . toggle (
138+ "chat-window-minimized"
139+ ) ;
140+ }
141+ }
142+ }
143+ )
144+ ) ;
145+
146+ // Add maximize button for desktop
147+ if ( isDesktop ) {
148+ chatHeader . appendChild (
149+ createButton (
150+ "Full screen" ,
151+ `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><path d="M15 3h6v6"></path><path d="m21 3-7 7"></path><path d="m3 21 7-7"></path><path d="M9 21H3v-6"></path></svg>` ,
152+ "mr-12" ,
153+ ( ) => {
154+ if ( chatWindowRef . current ) {
155+ if ( chatWindowRef . current ) {
156+ if (
157+ chatWindowRef . current . classList . contains (
158+ "chat-window-right"
159+ )
160+ ) {
161+ chatWindowRef . current . classList . remove (
162+ "chat-window-right"
163+ ) ;
164+ chatWindowRef . current . classList . add (
165+ "chat-window-centered"
166+ ) ;
167+ } else {
168+ chatWindowRef . current . classList . remove (
169+ "chat-window-centered"
170+ ) ;
171+ chatWindowRef . current . classList . add ( "chat-window-right" ) ;
172+ }
173+ }
174+ }
175+ }
176+ )
177+ ) ;
178+ }
179+ }
180+
181+ return ( ) => {
182+ chat . unmount ( ) ;
183+ } ;
184+ }
185+ } , [ isDesktop ] ) ;
186+
187+ // Effect to handle mobile FAB
188+ useEffect ( ( ) => {
189+ if ( ! isDesktop && chatFABRef . current ) {
190+ chatFABRef . current . addEventListener ( "click" , ( ) => {
191+ chatFABRef . current ?. setAttribute ( "style" , "display:none;" ) ;
192+ } ) ;
33193 }
34- } , [ ] ) ;
35194
36- return < > </ > ;
37- } ;
195+ return ( ) => {
196+ if ( ! isDesktop && chatFABRef . current ) {
197+ chatFABRef . current . removeEventListener ( "click" , ( ) => {
198+ chatFABRef . current ?. setAttribute ( "style" , "display:block;" ) ;
199+ } ) ;
200+ }
201+ } ;
202+ } , [ isDesktop ] ) ;
203+
204+ const handleSubmit = async ( text : string ) => {
205+ if ( ! text . length || ! chatDivRef . current ) return ;
206+
207+ setIsSubmitted ( true ) ;
208+ const textarea = chatDivRef . current . querySelector ( "textarea" ) ;
209+ if ( ! textarea ) return ;
210+
211+ textarea . value = text ;
212+ textarea . dispatchEvent ( new Event ( "input" , { bubbles : true } ) ) ;
213+
214+ const waitForSendButton = setInterval ( ( ) => {
215+ const sendButton = chatDivRef . current ?. querySelector (
216+ ".chat-input-send-button"
217+ ) ;
218+ if ( sendButton && ! sendButton . hasAttribute ( "disabled" ) ) {
219+ clearInterval ( waitForSendButton ) ;
220+ triggerClick ( sendButton ) ;
221+
222+ if ( chatWindowRef . current ) {
223+ chatWindowRef . current . classList . toggle ( "chat-window-minimized" ) ;
224+ chatWindowRef . current . classList . toggle ( "chat-window-centered" ) ;
225+ }
226+
227+ if ( chatFABRef . current ) {
228+ triggerClick ( chatFABRef . current ) ;
229+ textarea . value = "" ;
230+ setText ( "" ) ;
231+ setIsSubmitted ( true ) ;
232+ setIsChatOpen ( true ) ;
233+ }
234+ }
235+ } , 100 ) ;
236+
237+ setTimeout ( ( ) => clearInterval ( waitForSendButton ) , 5000 ) ;
238+ } ;
239+
240+ return (
241+ < >
242+ < div
243+ className = { cn (
244+ "animate-fade-in-up fixed bottom-0 z-50 mb-8 hidden w-full items-center px-4 transition-all" ,
245+ isSubmitted ? "hidden" : "lg:flex"
246+ ) } >
247+ < div className = "mx-auto flex w-full max-w-4xl flex-col gap-2 rounded-xl bg-[url('/images/ai-agent.png')] bg-contain bg-center p-4 shadow-xl" >
248+ < div className = "flex flex-row items-center gap-2" >
249+ < div className = "relative flex w-full items-center" >
250+ < SparklesIcon
251+ strokeWidth = { 1.5 }
252+ className = "absolute left-0 z-10 ml-4 h-6 w-6 text-[#3276F5]"
253+ />
254+ < input
255+ type = "text"
256+ className = "z-0 h-[52px] w-full max-w-7xl rounded-lg bg-white p-2 pr-12 pl-12 text-gray-800 placeholder:text-gray-400"
257+ value = { text }
258+ onChange = { ( { target } ) => setText ( target . value ) }
259+ aria-describedby = "Ask me about services, success stories, or your challenges"
260+ placeholder = "Ask me about services, success stories, or your challenges"
261+ />
262+ < Button
263+ id = "desktop-ai-submit"
264+ type = "button"
265+ className = "absolute right-0 z-10 mr-4 ml-4 h-7 w-7 rounded-full bg-[#3276F5] p-2 hover:cursor-pointer hover:bg-[#3276F5DD]"
266+ onClick = { ( ) => handleSubmit ( text ) }
267+ disabled = { isSubmitted } >
268+ < div className = "flex items-center justify-center" >
269+ { isSubmitted ? (
270+ < LoaderCircle className = "h-4 w-4 animate-spin" />
271+ ) : (
272+ < Image
273+ alt = "Send message to AI"
274+ src = "/icons/ai-agent-button.svg"
275+ width = { 16 }
276+ height = { 16 }
277+ />
278+ ) }
279+ </ div >
280+ </ Button >
281+ </ div >
282+ </ div >
283+ < div className = "flex shrink-0 flex-row gap-2 overflow-auto" >
284+ { DEFAULT_MESSAGES . map ( ( { text, id } ) => (
285+ < Button
286+ key = { id }
287+ className = "rounded-md border border-white bg-transparent hover:cursor-pointer"
288+ onClick = { ( ) => setText ( text ) } >
289+ { text }
290+ </ Button >
291+ ) ) }
292+ </ div >
293+ </ div >
294+ </ div >
295+ < Button
296+ onClick = { ( ) => {
297+ const chatFAB = document . querySelector (
298+ "#n8n-chat .chat-window-toggle"
299+ ) ;
300+ if ( chatFAB ) {
301+ const clickEvent = new MouseEvent ( "click" , {
302+ bubbles : true ,
303+ cancelable : true ,
304+ view : window
305+ } ) ;
306+ chatFAB . dispatchEvent ( clickEvent ) ;
307+
308+ // Open the n8n chat window
309+ const chatDiv = document . querySelector ( "#n8n-chat" ) ;
310+ if ( chatDiv ) {
311+ const chatWindow = chatDiv . querySelector ( ".chat-window-wrapper" ) ;
312+ if ( chatWindow ) {
313+ chatWindow . classList . toggle ( "chat-window-minimized" ) ;
314+ }
315+ }
316+
317+ setIsChatOpen ( true ) ;
318+ }
319+ } }
320+ className = { cn (
321+ isChatOpen ? "hidden" : "lg:flex" ,
322+ "fixed right-0 bottom-0 z-50 mr-8 mb-4 hidden rounded-full bg-[#3276F5] p-2 px-4 font-bold hover:cursor-pointer hover:bg-[#3276F5DD]"
323+ ) } >
324+ < div className = "flex items-center justify-center" > Ask HyperBot</ div >
325+ </ Button >
326+ </ >
327+ ) ;
328+ }
0 commit comments