mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-07 03:48:44 +08:00
Add widget customization and persistence (#14603)
Introduce comprehensive floating widget customization: add new widget settings (title, subtitle, footer, colors, mute, streaming) with types and defaults, and expose them via EmbedDialog UI (split into Embed Setup and Widget Customization tabs). Persist and load settings through Agent page by reading/writing globals and wiring an onSaveWidgetSettings handler to setAgent; show a loading ButtonLoading for saving. Update embed iframe query params and FloatingChatWidget to honor URL params (colors, text, mute/streaming) with validation/normalization, color darkening for gradients, footer link normalization, and improved styling. Also add copy-to-clipboard in message toolbar, adjust syntax highlighter layout and Copy button, and add i18n key for muteWidget. ### What problem does this PR solve? Adds a few fields to the embed widget modal to customize the appearance of the floating widget when embedded into a page. ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Co-authored-by: Noah <Noah.Thompson@ecn.forces.gc.ca>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button, ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/components/ui/form';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import {
|
||||
LanguageAbbreviation,
|
||||
@@ -48,23 +49,78 @@ const FormSchema = z.object({
|
||||
locale: z.string(),
|
||||
embedType: z.enum(['fullscreen', 'widget']),
|
||||
enableStreaming: z.boolean(),
|
||||
muteWidget: z.boolean(),
|
||||
theme: z.enum([ThemeEnum.Light, ThemeEnum.Dark]),
|
||||
userId: z.string().optional(),
|
||||
widgetTitle: z.string(),
|
||||
widgetSubtitle: z.string(),
|
||||
widgetFooterText: z.string(),
|
||||
widgetFooterLink: z.string(),
|
||||
widgetAccentColor: z.string(),
|
||||
widgetBackgroundColor: z.string(),
|
||||
widgetTextColor: z.string(),
|
||||
widgetHeaderTextColor: z.string(),
|
||||
widgetFooterTextColor: z.string(),
|
||||
});
|
||||
|
||||
export type WidgetSettings = Pick<
|
||||
z.infer<typeof FormSchema>,
|
||||
| 'enableStreaming'
|
||||
| 'muteWidget'
|
||||
| 'widgetTitle'
|
||||
| 'widgetSubtitle'
|
||||
| 'widgetFooterText'
|
||||
| 'widgetFooterLink'
|
||||
| 'widgetAccentColor'
|
||||
| 'widgetBackgroundColor'
|
||||
| 'widgetTextColor'
|
||||
| 'widgetHeaderTextColor'
|
||||
| 'widgetFooterTextColor'
|
||||
>;
|
||||
|
||||
export const defaultWidgetSettings: WidgetSettings = {
|
||||
enableStreaming: false,
|
||||
muteWidget: false,
|
||||
widgetTitle: '',
|
||||
widgetSubtitle: '',
|
||||
widgetFooterText: '',
|
||||
widgetFooterLink: '',
|
||||
widgetAccentColor: '#2563eb',
|
||||
widgetBackgroundColor: '#ffffff',
|
||||
widgetTextColor: '#111827',
|
||||
widgetHeaderTextColor: '#ffffff',
|
||||
widgetFooterTextColor: '#111827',
|
||||
};
|
||||
|
||||
type IProps = IModalProps<any> & {
|
||||
token: string;
|
||||
from: SharedFrom;
|
||||
beta: string;
|
||||
isAgent: boolean;
|
||||
initialWidgetSettings?: Partial<WidgetSettings>;
|
||||
onSaveWidgetSettings?: (settings: WidgetSettings) => Promise<unknown>;
|
||||
savingWidgetSettings?: boolean;
|
||||
};
|
||||
|
||||
const normalizeHexColor = (value: string | undefined, fallback: string) => {
|
||||
const normalizedValue = value?.trim() ?? '';
|
||||
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(normalizedValue)
|
||||
? normalizedValue
|
||||
: fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the embed code preview and customization UI for shared chat and agent widgets.
|
||||
*/
|
||||
function EmbedDialog({
|
||||
hideModal,
|
||||
token = '',
|
||||
from,
|
||||
beta = '',
|
||||
isAgent,
|
||||
initialWidgetSettings,
|
||||
onSaveWidgetSettings,
|
||||
savingWidgetSettings,
|
||||
visible,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -77,8 +133,9 @@ function EmbedDialog({
|
||||
published: false,
|
||||
locale: '',
|
||||
embedType: 'fullscreen' as const,
|
||||
enableStreaming: false,
|
||||
theme: ThemeEnum.Light,
|
||||
...defaultWidgetSettings,
|
||||
...initialWidgetSettings,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,8 +155,18 @@ function EmbedDialog({
|
||||
locale,
|
||||
embedType,
|
||||
enableStreaming,
|
||||
muteWidget,
|
||||
theme,
|
||||
userId,
|
||||
widgetTitle,
|
||||
widgetSubtitle,
|
||||
widgetFooterText,
|
||||
widgetFooterLink,
|
||||
widgetAccentColor,
|
||||
widgetBackgroundColor,
|
||||
widgetTextColor,
|
||||
widgetHeaderTextColor,
|
||||
widgetFooterTextColor,
|
||||
} = values;
|
||||
const baseRoute =
|
||||
embedType === 'widget'
|
||||
@@ -125,6 +192,39 @@ function EmbedDialog({
|
||||
if (embedType === 'widget') {
|
||||
src.searchParams.append('mode', 'master');
|
||||
src.searchParams.append('streaming', String(enableStreaming));
|
||||
src.searchParams.append('muted', String(muteWidget));
|
||||
if (!isEmpty(trim(widgetTitle))) {
|
||||
src.searchParams.append('widget_title', widgetTitle ?? '');
|
||||
}
|
||||
if (!isEmpty(trim(widgetSubtitle))) {
|
||||
src.searchParams.append('widget_subtitle', widgetSubtitle ?? '');
|
||||
}
|
||||
if (!isEmpty(trim(widgetFooterText))) {
|
||||
src.searchParams.append('widget_footer', widgetFooterText ?? '');
|
||||
}
|
||||
if (!isEmpty(trim(widgetFooterLink))) {
|
||||
src.searchParams.append('widget_footer_link', widgetFooterLink ?? '');
|
||||
}
|
||||
src.searchParams.append(
|
||||
'widget_accent_color',
|
||||
normalizeHexColor(widgetAccentColor, '#2563eb'),
|
||||
);
|
||||
src.searchParams.append(
|
||||
'widget_background_color',
|
||||
normalizeHexColor(widgetBackgroundColor, '#ffffff'),
|
||||
);
|
||||
src.searchParams.append(
|
||||
'widget_text_color',
|
||||
normalizeHexColor(widgetTextColor, '#111827'),
|
||||
);
|
||||
src.searchParams.append(
|
||||
'widget_header_text_color',
|
||||
normalizeHexColor(widgetHeaderTextColor, '#ffffff'),
|
||||
);
|
||||
src.searchParams.append(
|
||||
'widget_footer_text_color',
|
||||
normalizeHexColor(widgetFooterTextColor, '#111827'),
|
||||
);
|
||||
}
|
||||
if (theme && embedType === 'fullscreen') {
|
||||
src.searchParams.append('theme', theme);
|
||||
@@ -179,9 +279,29 @@ window.addEventListener('message',e=>{
|
||||
window.open(iframeSrc, '_blank');
|
||||
}, [generateIframeSrc]);
|
||||
|
||||
const handleSaveWidgetSettings = useCallback(async () => {
|
||||
if (!onSaveWidgetSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onSaveWidgetSettings({
|
||||
enableStreaming: values.enableStreaming,
|
||||
muteWidget: values.muteWidget,
|
||||
widgetTitle: values.widgetTitle,
|
||||
widgetSubtitle: values.widgetSubtitle,
|
||||
widgetFooterText: values.widgetFooterText,
|
||||
widgetFooterLink: values.widgetFooterLink,
|
||||
widgetAccentColor: values.widgetAccentColor,
|
||||
widgetBackgroundColor: values.widgetBackgroundColor,
|
||||
widgetTextColor: values.widgetTextColor,
|
||||
widgetHeaderTextColor: values.widgetHeaderTextColor,
|
||||
widgetFooterTextColor: values.widgetFooterTextColor,
|
||||
});
|
||||
}, [onSaveWidgetSettings, values]);
|
||||
|
||||
return (
|
||||
<Dialog open={visible} onOpenChange={hideModal}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.embedIntoSite')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -189,103 +309,274 @@ window.addEventListener('message',e=>{
|
||||
<section className="w-full overflow-auto space-y-5 text-sm text-text-secondary">
|
||||
<Form {...form}>
|
||||
<form className="space-y-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="embedType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('chat.embedType')}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="fullscreen" id="fullscreen" />
|
||||
<Label htmlFor="fullscreen" className="text-sm">
|
||||
{t('chat.fullscreenChat')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="widget" id="widget" />
|
||||
<Label htmlFor="widget" className="text-sm">
|
||||
{t('chat.floatingWidget')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{values.embedType === 'fullscreen' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('chat.theme')}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-row space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={ThemeEnum.Light}
|
||||
id="light"
|
||||
/>
|
||||
<Label htmlFor="light" className="text-sm">
|
||||
{t('chat.light')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={ThemeEnum.Dark} id="dark" />
|
||||
<Label htmlFor="dark" className="text-sm">
|
||||
{t('chat.dark')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<Tabs defaultValue="embed" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="embed">Embed Setup</TabsTrigger>
|
||||
<TabsTrigger value="widget">Widget Customization</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="embed" className="space-y-5">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="embedType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('chat.embedType')}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-col space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value="fullscreen"
|
||||
id="fullscreen"
|
||||
/>
|
||||
<Label htmlFor="fullscreen" className="text-sm">
|
||||
{t('chat.fullscreenChat')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="widget" id="widget" />
|
||||
<Label htmlFor="widget" className="text-sm">
|
||||
{t('chat.floatingWidget')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{values.embedType === 'fullscreen' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('chat.theme')}</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
className="flex flex-row space-x-4"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={ThemeEnum.Light}
|
||||
id="light"
|
||||
/>
|
||||
<Label htmlFor="light" className="text-sm">
|
||||
{t('chat.light')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem
|
||||
value={ThemeEnum.Dark}
|
||||
id="dark"
|
||||
/>
|
||||
<Label htmlFor="dark" className="text-sm">
|
||||
{t('chat.dark')}
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<SwitchFormField
|
||||
name="visibleAvatar"
|
||||
label={t('chat.avatarHidden')}
|
||||
></SwitchFormField>
|
||||
{isAgent && (
|
||||
<SwitchFormField
|
||||
name="published"
|
||||
label={t('chat.published')}
|
||||
tooltip={t('chat.publishedTooltip')}
|
||||
></SwitchFormField>
|
||||
)}
|
||||
{values.embedType === 'widget' && (
|
||||
<SwitchFormField
|
||||
name="enableStreaming"
|
||||
label={t('chat.enableStreaming')}
|
||||
></SwitchFormField>
|
||||
)}
|
||||
<RAGFlowFormItem name="locale" label={t('chat.locale')}>
|
||||
<SelectWithSearch options={languageOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
{isAgent && (
|
||||
<RAGFlowFormItem name="userId" label={t('flow.userId')}>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
<SwitchFormField
|
||||
name="visibleAvatar"
|
||||
label={t('chat.avatarHidden')}
|
||||
></SwitchFormField>
|
||||
{isAgent && (
|
||||
<SwitchFormField
|
||||
name="published"
|
||||
label={t('chat.published')}
|
||||
tooltip={t('chat.publishedTooltip')}
|
||||
></SwitchFormField>
|
||||
)}
|
||||
{values.embedType === 'widget' && (
|
||||
<SwitchFormField
|
||||
name="enableStreaming"
|
||||
label={t('chat.enableStreaming')}
|
||||
></SwitchFormField>
|
||||
)}
|
||||
{values.embedType === 'widget' && (
|
||||
<SwitchFormField
|
||||
name="muteWidget"
|
||||
label={t('chat.muteWidget')}
|
||||
></SwitchFormField>
|
||||
)}
|
||||
<RAGFlowFormItem name="locale" label={t('chat.locale')}>
|
||||
<SelectWithSearch
|
||||
options={languageOptions}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
{isAgent && (
|
||||
<RAGFlowFormItem name="userId" label={t('flow.userId')}>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="widget" className="space-y-5">
|
||||
<div className="rounded-md border border-border p-3 text-xs text-text-secondary">
|
||||
These settings apply to the floating widget embed.
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<RAGFlowFormItem name="widgetTitle" label="Widget title">
|
||||
<Input placeholder="Chat Support"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem name="widgetSubtitle" label="Subtitle">
|
||||
<Input placeholder="We typically reply instantly"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="widgetFooterText"
|
||||
label="Footer text"
|
||||
>
|
||||
<Input placeholder="Powered by RAGFlow"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="widgetFooterLink"
|
||||
label="Footer redirect link"
|
||||
>
|
||||
<Input placeholder="https://ragflow.io"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="widgetAccentColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Widget accent color</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={normalizeHexColor(
|
||||
field.value,
|
||||
'#2563eb',
|
||||
)}
|
||||
onChange={field.onChange}
|
||||
className="h-10 w-16 p-1"
|
||||
/>
|
||||
<Input {...field}></Input>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="widgetBackgroundColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Background color</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={normalizeHexColor(
|
||||
field.value,
|
||||
'#ffffff',
|
||||
)}
|
||||
onChange={field.onChange}
|
||||
className="h-10 w-16 p-1"
|
||||
/>
|
||||
<Input {...field}></Input>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="widgetTextColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Text color</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={normalizeHexColor(
|
||||
field.value,
|
||||
'#111827',
|
||||
)}
|
||||
onChange={field.onChange}
|
||||
className="h-10 w-16 p-1"
|
||||
/>
|
||||
<Input {...field}></Input>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="widgetHeaderTextColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Header text color</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={normalizeHexColor(
|
||||
field.value,
|
||||
'#ffffff',
|
||||
)}
|
||||
onChange={field.onChange}
|
||||
className="h-10 w-16 p-1"
|
||||
/>
|
||||
<Input {...field}></Input>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="widgetFooterTextColor"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Footer text color</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
value={normalizeHexColor(
|
||||
field.value,
|
||||
'#111827',
|
||||
)}
|
||||
onChange={field.onChange}
|
||||
className="h-10 w-16 p-1"
|
||||
/>
|
||||
<Input {...field}></Input>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</form>
|
||||
</Form>
|
||||
<div>
|
||||
<span>{t('search.embedCode')}</span>
|
||||
<div>
|
||||
<div className="relative">
|
||||
<CopyToClipboard
|
||||
text={text}
|
||||
className="absolute right-3 top-3 z-10 border border-border bg-background/90 backdrop-blur-sm"
|
||||
/>
|
||||
<SyntaxHighlighter
|
||||
className="max-h-[350px] overflow-auto scrollbar-auto"
|
||||
className="max-h-[350px] overflow-auto scrollbar-auto pr-14"
|
||||
language="html"
|
||||
style={isDarkTheme ? oneDark : oneLight}
|
||||
>
|
||||
@@ -293,14 +584,26 @@ window.addEventListener('message',e=>{
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleOpenInNewTab}
|
||||
className="w-full"
|
||||
variant="secondary"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('common.openInNewTab')}
|
||||
</Button>
|
||||
<div className="flex gap-3">
|
||||
{isAgent && onSaveWidgetSettings && (
|
||||
<ButtonLoading
|
||||
onClick={handleSaveWidgetSettings}
|
||||
loading={savingWidgetSettings}
|
||||
className="flex-1"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('flow.save')} widget settings
|
||||
</ButtonLoading>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleOpenInNewTab}
|
||||
className={isAgent && onSaveWidgetSettings ? 'flex-1' : 'w-full'}
|
||||
variant="secondary"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
{t('common.openInNewTab')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className=" font-medium mt-4 mb-1">
|
||||
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
|
||||
<span className="ml-1 inline-block">ID</span>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import PdfSheet from '@/components/pdf-drawer';
|
||||
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
||||
import { MessageType, SharedFrom } from '@/constants/chat';
|
||||
@@ -14,6 +15,75 @@ import {
|
||||
} from '../pages/next-chats/hooks/use-send-shared-message';
|
||||
import FloatingChatWidgetMarkdown from './floating-chat-widget-markdown';
|
||||
|
||||
/**
|
||||
* Normalizes a hex color input and falls back to a safe default when invalid.
|
||||
*/
|
||||
const normalizeHexColor = (value: string | null, fallback: string) => {
|
||||
return value && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)
|
||||
? value
|
||||
: fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Darkens a hex color to derive hover and gradient variants for the widget chrome.
|
||||
*/
|
||||
const darkenHexColor = (hexColor: string, amount = 0.12) => {
|
||||
const normalizedHex = hexColor.replace('#', '');
|
||||
const expandedHex =
|
||||
normalizedHex.length === 3
|
||||
? normalizedHex
|
||||
.split('')
|
||||
.map((char) => `${char}${char}`)
|
||||
.join('')
|
||||
: normalizedHex;
|
||||
const channels = expandedHex.match(/.{2}/g);
|
||||
|
||||
if (!channels) {
|
||||
return hexColor;
|
||||
}
|
||||
|
||||
return `#${channels
|
||||
.map((channel) => {
|
||||
const value = parseInt(channel, 16);
|
||||
const adjustedValue = Math.max(
|
||||
0,
|
||||
Math.min(255, Math.round(value * (1 - amount))),
|
||||
);
|
||||
return adjustedValue.toString(16).padStart(2, '0');
|
||||
})
|
||||
.join('')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Accepts a footer link from the widget query string and returns a safe HTTP(S) URL.
|
||||
*/
|
||||
const normalizeWidgetFooterLink = (value: string | null) => {
|
||||
const normalizedValue = value?.trim();
|
||||
|
||||
if (!normalizedValue) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const candidate = /^[a-z][a-z\d+.-]*:/i.test(normalizedValue)
|
||||
? normalizedValue
|
||||
: `https://${normalizedValue}`;
|
||||
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
return url.toString();
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the embeddable floating chat widget and applies URL-driven widget settings.
|
||||
*/
|
||||
const FloatingChatWidget = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -36,6 +106,34 @@ const FloatingChatWidget = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const mode = urlParams.get('mode') || 'full'; // 'button', 'window', or 'full'
|
||||
const enableStreaming = urlParams.get('streaming') === 'true'; // Only enable if explicitly set to true
|
||||
const isMuted = urlParams.get('muted') === 'true';
|
||||
const widgetTitle = urlParams.get('widget_title')?.trim();
|
||||
const widgetSubtitle = urlParams.get('widget_subtitle')?.trim();
|
||||
const widgetFooter = urlParams.get('widget_footer')?.trim();
|
||||
const widgetFooterLink = normalizeWidgetFooterLink(
|
||||
urlParams.get('widget_footer_link'),
|
||||
);
|
||||
const widgetAccentColor = normalizeHexColor(
|
||||
urlParams.get('widget_accent_color'),
|
||||
'#2563eb',
|
||||
);
|
||||
const widgetAccentColorStrong = darkenHexColor(widgetAccentColor);
|
||||
const widgetBackgroundColor = normalizeHexColor(
|
||||
urlParams.get('widget_background_color'),
|
||||
'#ffffff',
|
||||
);
|
||||
const widgetTextColor = normalizeHexColor(
|
||||
urlParams.get('widget_text_color'),
|
||||
'#111827',
|
||||
);
|
||||
const widgetHeaderTextColor = normalizeHexColor(
|
||||
urlParams.get('widget_header_text_color'),
|
||||
'#ffffff',
|
||||
);
|
||||
const widgetFooterTextColor = normalizeHexColor(
|
||||
urlParams.get('widget_footer_text_color'),
|
||||
'#111827',
|
||||
);
|
||||
|
||||
const {
|
||||
handlePressEnter,
|
||||
@@ -58,6 +156,49 @@ const FloatingChatWidget = () => {
|
||||
)();
|
||||
|
||||
const title = data.title;
|
||||
const displayTitle = widgetTitle || title || t('chat.chatSupport');
|
||||
const displaySubtitle = widgetSubtitle || t('chat.replyInstantly');
|
||||
const displayFooter = widgetFooter || '';
|
||||
const renderFooter = () => {
|
||||
if (!displayFooter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="mt-3 -mx-4 -mb-4 px-4 py-3 text-center text-xs"
|
||||
style={{
|
||||
backgroundColor: widgetAccentColor,
|
||||
color: widgetFooterTextColor,
|
||||
}}
|
||||
>
|
||||
{widgetFooterLink ? (
|
||||
<a
|
||||
href={widgetFooterLink}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:opacity-90"
|
||||
style={{ color: widgetFooterTextColor }}
|
||||
>
|
||||
{displayFooter}
|
||||
</a>
|
||||
) : (
|
||||
displayFooter
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const bodyContainerStyle: React.CSSProperties = {
|
||||
borderRadius: '0 0 16px 16px',
|
||||
backgroundColor: widgetBackgroundColor,
|
||||
color: widgetTextColor,
|
||||
};
|
||||
const inputStyle: React.CSSProperties = {
|
||||
minHeight: '44px',
|
||||
maxHeight: '120px',
|
||||
color: widgetTextColor,
|
||||
backgroundColor: widgetBackgroundColor,
|
||||
};
|
||||
|
||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||
useClickDrawer();
|
||||
@@ -69,6 +210,10 @@ const FloatingChatWidget = () => {
|
||||
|
||||
// Play sound when opening
|
||||
const playNotificationSound = useCallback(() => {
|
||||
if (isMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const audioContext = new (
|
||||
window.AudioContext || (window as any).webkitAudioContext
|
||||
@@ -94,10 +239,14 @@ const FloatingChatWidget = () => {
|
||||
console.warn(error);
|
||||
// Silent fail if audio not supported
|
||||
}
|
||||
}, []);
|
||||
}, [isMuted]);
|
||||
|
||||
// Play sound for AI responses (Intercom-style)
|
||||
const playResponseSound = useCallback(() => {
|
||||
if (isMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const audioContext = new (
|
||||
window.AudioContext || (window as any).webkitAudioContext
|
||||
@@ -124,7 +273,7 @@ const FloatingChatWidget = () => {
|
||||
|
||||
// Silent fail if audio not supported
|
||||
}
|
||||
}, []);
|
||||
}, [isMuted]);
|
||||
|
||||
// Set loaded state and locale
|
||||
useEffect(() => {
|
||||
@@ -338,9 +487,10 @@ const FloatingChatWidget = () => {
|
||||
'*',
|
||||
);
|
||||
}}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
className={`w-14 h-14 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
@@ -368,9 +518,10 @@ const FloatingChatWidget = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChat}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
className={`w-14 h-14 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
@@ -394,30 +545,36 @@ const FloatingChatWidget = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed top-0 left-0 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out h-[500px] w-[380px] overflow-hidden ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
className={`fixed top-0 left-0 z-50 rounded-2xl transition-all duration-300 ease-out h-[500px] w-[380px] overflow-hidden ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
|
||||
<div
|
||||
className="flex items-center justify-between p-4 text-white rounded-t-2xl"
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${widgetAccentColor}, ${widgetAccentColorStrong})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
|
||||
<MessageCircle size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{title || t('chat.chatSupport')}
|
||||
<h3
|
||||
className="font-semibold text-sm"
|
||||
style={{ color: widgetHeaderTextColor }}
|
||||
>
|
||||
{displayTitle}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
{t('chat.replyInstantly')}
|
||||
<p className="text-xs" style={{ color: widgetHeaderTextColor }}>
|
||||
{displaySubtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages and Input */}
|
||||
<div
|
||||
className="flex flex-col h-[436px] bg-white"
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
>
|
||||
<div className="flex flex-col h-[436px]" style={bodyContainerStyle}>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
onWheel={(e) => {
|
||||
@@ -447,29 +604,49 @@ const FloatingChatWidget = () => {
|
||||
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
className={`group max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
message.role === MessageType.User
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 text-gray-800 rounded-bl-md'
|
||||
? 'text-white rounded-br-md'
|
||||
: 'rounded-bl-md'
|
||||
}`}
|
||||
style={
|
||||
message.role === MessageType.User
|
||||
? { backgroundColor: widgetAccentColor }
|
||||
: {
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.14)',
|
||||
color: widgetTextColor,
|
||||
}
|
||||
}
|
||||
>
|
||||
{message.role === MessageType.User ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
) : (
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
<div className="space-y-2">
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100"
|
||||
role="toolbar"
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={message.content}
|
||||
className="border-0"
|
||||
size="icon-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -479,14 +656,23 @@ const FloatingChatWidget = () => {
|
||||
{sendLoading && !enableStreaming && (
|
||||
<div className="flex justify-start pl-4">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.1s' }}
|
||||
className="w-2 h-2 rounded-full animate-bounce"
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 bg-blue-500 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0.2s' }}
|
||||
className="w-2 h-2 rounded-full animate-bounce"
|
||||
style={{
|
||||
backgroundColor: widgetAccentColor,
|
||||
animationDelay: '0.1s',
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full animate-bounce"
|
||||
style={{
|
||||
backgroundColor: widgetAccentColor,
|
||||
animationDelay: '0.2s',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,7 +682,10 @@ const FloatingChatWidget = () => {
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
<div
|
||||
className="border-t border-gray-200 p-4"
|
||||
style={bodyContainerStyle}
|
||||
>
|
||||
<div className="flex items-end space-x-3">
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
@@ -509,8 +698,8 @@ const FloatingChatWidget = () => {
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder={t('chat.typeYourMessage')}
|
||||
rows={1}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
|
||||
style={{ minHeight: '44px', maxHeight: '120px' }}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:border-transparent"
|
||||
style={inputStyle}
|
||||
disabled={hasError || sendLoading}
|
||||
/>
|
||||
</div>
|
||||
@@ -518,11 +707,13 @@ const FloatingChatWidget = () => {
|
||||
type="button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue.trim() || sendLoading}
|
||||
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="p-3 text-white rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{renderFooter()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -546,22 +737,31 @@ const FloatingChatWidget = () => {
|
||||
{/* Chat Widget Container */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className={`fixed bottom-24 right-6 z-50 bg-blue-600 rounded-2xl transition-all duration-300 ease-out ${
|
||||
className={`fixed bottom-24 right-6 z-50 rounded-2xl transition-all duration-300 ease-out ${
|
||||
isMinimized ? 'h-16' : 'h-[500px]'
|
||||
} w-[380px] overflow-hidden`}
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-t-2xl">
|
||||
<div
|
||||
className="flex items-center justify-between p-4 text-white rounded-t-2xl"
|
||||
style={{
|
||||
background: `linear-gradient(to right, ${widgetAccentColor}, ${widgetAccentColorStrong})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center">
|
||||
<MessageCircle size={18} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-sm">
|
||||
{title || t('chat.chatSupport')}
|
||||
<h3
|
||||
className="font-semibold text-sm"
|
||||
style={{ color: widgetHeaderTextColor }}
|
||||
>
|
||||
{displayTitle}
|
||||
</h3>
|
||||
<p className="text-xs text-blue-100">
|
||||
{t('chat.replyInstantly')}
|
||||
<p className="text-xs" style={{ color: widgetHeaderTextColor }}>
|
||||
{displaySubtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -585,10 +785,7 @@ const FloatingChatWidget = () => {
|
||||
|
||||
{/* Messages Container */}
|
||||
{!isMinimized && (
|
||||
<div
|
||||
className="flex flex-col h-[436px] bg-white"
|
||||
style={{ borderRadius: '0 0 16px 16px' }}
|
||||
>
|
||||
<div className="flex flex-col h-[436px]" style={bodyContainerStyle}>
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-4 space-y-4"
|
||||
onWheel={(e) => {
|
||||
@@ -621,29 +818,49 @@ const FloatingChatWidget = () => {
|
||||
className={`flex ${message.role === MessageType.User ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
className={`group max-w-[280px] px-4 py-2 rounded-2xl ${
|
||||
message.role === MessageType.User
|
||||
? 'bg-blue-600 text-white rounded-br-md'
|
||||
: 'bg-gray-100 text-gray-800 rounded-bl-md'
|
||||
? 'text-white rounded-br-md'
|
||||
: 'rounded-bl-md'
|
||||
}`}
|
||||
style={
|
||||
message.role === MessageType.User
|
||||
? { backgroundColor: widgetAccentColor }
|
||||
: {
|
||||
backgroundColor: 'rgba(148, 163, 184, 0.14)',
|
||||
color: widgetTextColor,
|
||||
}
|
||||
}
|
||||
>
|
||||
{message.role === MessageType.User ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{message.content}
|
||||
</p>
|
||||
) : (
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
<div className="space-y-2">
|
||||
<FloatingChatWidgetMarkdown
|
||||
loading={false}
|
||||
content={message.content}
|
||||
reference={
|
||||
message.reference || {
|
||||
doc_aggs: [],
|
||||
chunks: [],
|
||||
total: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
clickDocumentButton={clickDocumentButton}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-end opacity-0 transition-opacity group-hover:opacity-100"
|
||||
role="toolbar"
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={message.content}
|
||||
className="border-0"
|
||||
size="icon-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -686,8 +903,8 @@ const FloatingChatWidget = () => {
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder={t('chat.typeYourMessage')}
|
||||
rows={1}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black"
|
||||
style={{ minHeight: '44px', maxHeight: '120px' }}
|
||||
className="w-full resize-none border border-gray-300 rounded-2xl px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:border-transparent"
|
||||
style={inputStyle}
|
||||
disabled={hasError || sendLoading}
|
||||
/>
|
||||
</div>
|
||||
@@ -695,11 +912,13 @@ const FloatingChatWidget = () => {
|
||||
type="button"
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue.trim() || sendLoading}
|
||||
className="p-3 bg-blue-600 text-white rounded-full hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
className="p-3 text-white rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
<Send size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{renderFooter()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -711,9 +930,10 @@ const FloatingChatWidget = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleChat}
|
||||
className={`w-14 h-14 bg-blue-600 hover:bg-blue-700 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
className={`w-14 h-14 text-white rounded-full transition-all duration-300 flex items-center justify-center group ${
|
||||
isOpen ? 'scale-95' : 'scale-100 hover:scale-105'
|
||||
}`}
|
||||
style={{ backgroundColor: widgetAccentColor }}
|
||||
>
|
||||
<div
|
||||
className={`transition-transform duration-300 ${isOpen ? 'rotate-45' : 'rotate-0'}`}
|
||||
|
||||
@@ -1078,6 +1078,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
enableStreaming: 'Enable streaming responses',
|
||||
muteWidget: 'Mute widget sounds',
|
||||
comingSoon: 'Coming soon',
|
||||
fullScreenTitle: 'Full embed',
|
||||
fullScreenDescription:
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import EmbedDialog from '@/components/embed-dialog';
|
||||
import EmbedDialog, {
|
||||
defaultWidgetSettings,
|
||||
type WidgetSettings,
|
||||
} from '@/components/embed-dialog';
|
||||
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import {
|
||||
@@ -21,6 +24,7 @@ import message from '@/components/ui/message';
|
||||
import { SharedFrom } from '@/constants/chat';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { useSetAgent } from '@/hooks/use-agent-request';
|
||||
import { ReactFlowProvider } from '@xyflow/react';
|
||||
import {
|
||||
ChevronDown,
|
||||
@@ -34,7 +38,7 @@ import {
|
||||
Settings,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||
import { ComponentPropsWithoutRef, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
import AgentCanvas from './canvas';
|
||||
@@ -42,6 +46,7 @@ import { DropdownProvider } from './canvas/context';
|
||||
import { PublishConfirmDialog } from './components/publish-confirm-dialog';
|
||||
import { Operator } from './constant';
|
||||
import { GlobalParamSheet } from './gobal-variable-sheet';
|
||||
import { useBuildDslData } from './hooks/use-build-dsl';
|
||||
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
|
||||
import { useHandleExportJsonFile } from './hooks/use-export-json';
|
||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||
@@ -66,6 +71,9 @@ import { useAgentHistoryManager } from './use-agent-history-manager';
|
||||
import { VersionDialog } from './version-dialog';
|
||||
import WebhookSheet from './webhook-sheet';
|
||||
|
||||
/**
|
||||
* Standardizes dropdown menu item styling for agent management actions.
|
||||
*/
|
||||
function AgentDropdownMenuItem({
|
||||
children,
|
||||
...props
|
||||
@@ -77,6 +85,11 @@ function AgentDropdownMenuItem({
|
||||
);
|
||||
}
|
||||
|
||||
const AgentWidgetSettingsGlobalKey = 'sys.widget_settings';
|
||||
|
||||
/**
|
||||
* Displays the agent editor and persists agent-scoped widget defaults in DSL globals.
|
||||
*/
|
||||
export default function Agent() {
|
||||
const { id } = useParams();
|
||||
const isPipeline = useIsPipeline();
|
||||
@@ -92,6 +105,8 @@ export default function Agent() {
|
||||
const { handleExportJson } = useHandleExportJsonFile();
|
||||
const { saveGraph, loading } = useSaveGraph();
|
||||
const { flowDetail: agentDetail } = useFetchDataOnMount();
|
||||
const { buildDslData } = useBuildDslData();
|
||||
const { setAgent, loading: savingWidgetSettings } = useSetAgent(false);
|
||||
const inputs = useGetBeginNodeDataInputs();
|
||||
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
|
||||
const handleRunAgent = useCallback(() => {
|
||||
@@ -211,6 +226,37 @@ export default function Agent() {
|
||||
uploadedFileData,
|
||||
} = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId });
|
||||
|
||||
const initialWidgetSettings = useMemo<WidgetSettings>(() => {
|
||||
const widgetSettings =
|
||||
agentDetail?.dsl?.globals?.[AgentWidgetSettingsGlobalKey];
|
||||
|
||||
return {
|
||||
...defaultWidgetSettings,
|
||||
...(widgetSettings && typeof widgetSettings === 'object'
|
||||
? widgetSettings
|
||||
: {}),
|
||||
};
|
||||
}, [agentDetail]);
|
||||
|
||||
const handleSaveWidgetSettings = useCallback(
|
||||
async (widgetSettings: WidgetSettings) => {
|
||||
const dsl = buildDslData();
|
||||
|
||||
return setAgent({
|
||||
id: id!,
|
||||
title: agentDetail.title,
|
||||
dsl: {
|
||||
...dsl,
|
||||
globals: {
|
||||
...dsl.globals,
|
||||
[AgentWidgetSettingsGlobalKey]: widgetSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[agentDetail.title, buildDslData, id, setAgent],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="h-full" data-testid="agent-detail">
|
||||
<PageHeader>
|
||||
@@ -327,6 +373,9 @@ export default function Agent() {
|
||||
from={SharedFrom.Agent}
|
||||
beta={beta}
|
||||
isAgent
|
||||
initialWidgetSettings={initialWidgetSettings}
|
||||
onSaveWidgetSettings={handleSaveWidgetSettings}
|
||||
savingWidgetSettings={savingWidgetSettings}
|
||||
></EmbedDialog>
|
||||
)}
|
||||
{versionDialogVisible && (
|
||||
|
||||
Reference in New Issue
Block a user