From 2e4295d5cac08643badfc838ae3e366609c703e9 Mon Sep 17 00:00:00 2001 From: Mohamed Mathari <155896313+melmathari@users.noreply.github.com> Date: Mon, 22 Sep 2025 05:03:33 +0200 Subject: [PATCH] Chat Widget (#10187) ### What problem does this PR solve? Add a chat widget. I'll probably need some assistance to get this ready for merge! ### Type of change - [x] New Feature (non-breaking change which adds functionality) Co-authored-by: Mohamed Mathari --- chat_demo/index.html | 19 + chat_demo/widget_demo.html | 154 +++++ web/src/components/embed-dialog/index.tsx | 109 +++- web/src/components/floating-chat-widget.tsx | 666 ++++++++++++++++++++ web/src/pages/next-chats/widget/index.tsx | 27 + web/src/routes.ts | 6 + 6 files changed, 975 insertions(+), 6 deletions(-) create mode 100644 chat_demo/index.html create mode 100644 chat_demo/widget_demo.html create mode 100644 web/src/components/floating-chat-widget.tsx create mode 100644 web/src/pages/next-chats/widget/index.tsx diff --git a/chat_demo/index.html b/chat_demo/index.html new file mode 100644 index 000000000..114b13683 --- /dev/null +++ b/chat_demo/index.html @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/chat_demo/widget_demo.html b/chat_demo/widget_demo.html new file mode 100644 index 000000000..34c262b37 --- /dev/null +++ b/chat_demo/widget_demo.html @@ -0,0 +1,154 @@ + + + + + + Floating Chat Widget Demo + + + +
+

🚀 Floating Chat Widget Demo

+ +

+ Welcome to our demo page! This page simulates a real website with content. + Look for the floating chat button in the bottom-right corner - just like Intercom! +

+ +
+

🎯 Widget Features

+
    +
  • Floating button that stays visible while scrolling
  • +
  • Click to open/close the chat window
  • +
  • Minimize button to collapse the chat
  • +
  • Professional Intercom-style design
  • +
  • Unread message indicator (red badge)
  • +
  • Transparent background integration
  • +
  • Responsive design for all screen sizes
  • +
+
+ +

+ The chat widget is completely separate from your website's content and won't + interfere with your existing layout or functionality. It's designed to be + lightweight and performant. +

+ +

+ Try scrolling this page - notice how the chat button stays in position. + Click it to start a conversation with our AI assistant! +

+ +
+

🔧 Implementation

+
    +
  • Simple iframe embed - just copy and paste
  • +
  • No JavaScript dependencies required
  • +
  • Works on any website or platform
  • +
  • Customizable appearance and behavior
  • +
  • Secure and privacy-focused
  • +
+
+ +

+ This is just placeholder content to demonstrate how the widget integrates + seamlessly with your existing website content. The widget floats above + everything else without disrupting your user experience. +

+ +

+ 🎉 Ready to add this to your website? Get your embed code from the admin panel! +

+
+ + + + + \ No newline at end of file diff --git a/web/src/components/embed-dialog/index.tsx b/web/src/components/embed-dialog/index.tsx index 9aa389565..34197ada0 100644 --- a/web/src/components/embed-dialog/index.tsx +++ b/web/src/components/embed-dialog/index.tsx @@ -15,6 +15,8 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Switch } from '@/components/ui/switch'; import { SharedFrom } from '@/constants/chat'; import { @@ -32,6 +34,8 @@ import { z } from 'zod'; const FormSchema = z.object({ visibleAvatar: z.boolean(), locale: z.string(), + embedType: z.enum(['fullscreen', 'widget']), + enableStreaming: z.boolean(), }); type IProps = IModalProps & { @@ -55,6 +59,8 @@ function EmbedDialog({ defaultValues: { visibleAvatar: false, locale: '', + embedType: 'fullscreen' as const, + enableStreaming: false, }, }); @@ -68,20 +74,60 @@ function EmbedDialog({ }, []); const generateIframeSrc = useCallback(() => { - const { visibleAvatar, locale } = values; - let src = `${location.origin}${from === SharedFrom.Agent ? Routes.AgentShare : Routes.ChatShare}?shared_id=${token}&from=${from}&auth=${beta}`; + const { visibleAvatar, locale, embedType, enableStreaming } = values; + const baseRoute = + embedType === 'widget' + ? Routes.ChatWidget + : from === SharedFrom.Agent + ? Routes.AgentShare + : Routes.ChatShare; + let src = `${location.origin}${baseRoute}?shared_id=${token}&from=${from}&auth=${beta}`; if (visibleAvatar) { src += '&visible_avatar=1'; } if (locale) { src += `&locale=${locale}`; } + if (enableStreaming) { + src += '&streaming=true'; + } return src; }, [beta, from, token, values]); const text = useMemo(() => { const iframeSrc = generateIframeSrc(); - return ` + const { embedType } = values; + + if (embedType === 'widget') { + const { enableStreaming } = values; + const streamingParam = enableStreaming + ? '&streaming=true' + : '&streaming=false'; + return ` + ~~~ html + + +~~~ + `; + } else { + return ` ~~~ html