From 088b049b4cb4ed87579d6d8bca32442f56b8a959 Mon Sep 17 00:00:00 2001
From: Oranggge <63722098+Oranggge@users.noreply.github.com>
Date: Mon, 1 Dec 2025 02:49:28 +0100
Subject: [PATCH] Feature: embedded chat theme (#11581)
### What problem does this PR solve?
This PR closing feature request #11286.
It implements ability to choose the background theme of the _Full screen
chat_ which is Embed into webpage.
Looks like that:
It works similar to `Locale`, using url parameter to set the theme.
if the parameter is invalid then is using the default theme.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
---------
Co-authored-by: Your Name
---
web/src/components/embed-dialog/index.tsx | 43 ++++++++++++++++++-
web/src/components/theme-provider.tsx | 10 +++++
.../agent/hooks/use-send-shared-message.ts | 1 +
web/src/pages/agent/share/index.tsx | 3 ++
.../hooks/use-send-shared-message.ts | 1 +
web/src/pages/next-chats/share/index.tsx | 4 ++
6 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/web/src/components/embed-dialog/index.tsx b/web/src/components/embed-dialog/index.tsx
index 34197ada0..7f0ccd60f 100644
--- a/web/src/components/embed-dialog/index.tsx
+++ b/web/src/components/embed-dialog/index.tsx
@@ -22,6 +22,7 @@ import { SharedFrom } from '@/constants/chat';
import {
LanguageAbbreviation,
LanguageAbbreviationMap,
+ ThemeEnum,
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
@@ -36,6 +37,7 @@ const FormSchema = z.object({
locale: z.string(),
embedType: z.enum(['fullscreen', 'widget']),
enableStreaming: z.boolean(),
+ theme: z.enum([ThemeEnum.Light, ThemeEnum.Dark]),
});
type IProps = IModalProps & {
@@ -61,6 +63,7 @@ function EmbedDialog({
locale: '',
embedType: 'fullscreen' as const,
enableStreaming: false,
+ theme: ThemeEnum.Light,
},
});
@@ -74,7 +77,7 @@ function EmbedDialog({
}, []);
const generateIframeSrc = useCallback(() => {
- const { visibleAvatar, locale, embedType, enableStreaming } = values;
+ const { visibleAvatar, locale, embedType, enableStreaming, theme } = values;
const baseRoute =
embedType === 'widget'
? Routes.ChatWidget
@@ -91,6 +94,9 @@ function EmbedDialog({
if (enableStreaming) {
src += '&streaming=true';
}
+ if (theme && embedType === 'fullscreen') {
+ src += `&theme=${theme}`;
+ }
return src;
}, [beta, from, token, values]);
@@ -181,6 +187,41 @@ function EmbedDialog({
)}
/>
+ {values.embedType === 'fullscreen' && (
+ (
+
+ Theme
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ />
+ )}
{
+ if (theme && (theme === ThemeEnum.Light || theme === ThemeEnum.Dark)) {
+ setTheme(theme as ThemeEnum);
+ }
+ }, [theme, setTheme]);
+}
diff --git a/web/src/pages/agent/hooks/use-send-shared-message.ts b/web/src/pages/agent/hooks/use-send-shared-message.ts
index ab7f7d2fc..fe1e34d62 100644
--- a/web/src/pages/agent/hooks/use-send-shared-message.ts
+++ b/web/src/pages/agent/hooks/use-send-shared-message.ts
@@ -29,6 +29,7 @@ export const useGetSharedChatSearchParams = () => {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
+ theme: searchParams.get('theme'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
diff --git a/web/src/pages/agent/share/index.tsx b/web/src/pages/agent/share/index.tsx
index af3393ad2..039d8e5d5 100644
--- a/web/src/pages/agent/share/index.tsx
+++ b/web/src/pages/agent/share/index.tsx
@@ -4,6 +4,7 @@ import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/next-message-item';
import PdfSheet from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
+import { useSyncThemeFromParams } from '@/components/theme-provider';
import { MessageType } from '@/constants/chat';
import { useUploadCanvasFileWithProgress } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
@@ -25,8 +26,10 @@ const ChatContainer = () => {
const {
sharedId: conversationId,
locale,
+ theme,
visibleAvatar,
} = useGetSharedChatSearchParams();
+ useSyncThemeFromParams(theme);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
diff --git a/web/src/pages/next-chats/hooks/use-send-shared-message.ts b/web/src/pages/next-chats/hooks/use-send-shared-message.ts
index fb287ff7d..022998e70 100644
--- a/web/src/pages/next-chats/hooks/use-send-shared-message.ts
+++ b/web/src/pages/next-chats/hooks/use-send-shared-message.ts
@@ -33,6 +33,7 @@ export const useGetSharedChatSearchParams = () => {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
+ theme: searchParams.get('theme'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
diff --git a/web/src/pages/next-chats/share/index.tsx b/web/src/pages/next-chats/share/index.tsx
index e01671eed..c08d14feb 100644
--- a/web/src/pages/next-chats/share/index.tsx
+++ b/web/src/pages/next-chats/share/index.tsx
@@ -3,6 +3,7 @@ import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/message-item';
import PdfSheet from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
+import { useSyncThemeFromParams } from '@/components/theme-provider';
import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
import { useFetchFlowSSE } from '@/hooks/flow-hooks';
@@ -22,8 +23,10 @@ const ChatContainer = () => {
sharedId: conversationId,
from,
locale,
+ theme,
visibleAvatar,
} = useGetSharedChatSearchParams();
+ useSyncThemeFromParams(theme);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
@@ -52,6 +55,7 @@ const ChatContainer = () => {
i18n.changeLanguage(locale);
}
}, [locale, visibleAvatar]);
+
const { data: avatarData } = useFetchAvatar();
if (!conversationId) {