Files
ragflow/web/src/pages/agent/index.tsx
47NoahThompson 9e0f976729 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>
2026-05-13 21:13:11 +08:00

421 lines
12 KiB
TypeScript

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 {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button, ButtonLoading } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
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,
CirclePlay,
Compass,
History,
LaptopMinimalCheck,
Logs,
MessageSquareCode,
ScreenShare,
Settings,
Upload,
} from 'lucide-react';
import { ComponentPropsWithoutRef, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import AgentCanvas from './canvas';
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';
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
import { useIsPipeline } from './hooks/use-is-pipeline';
import {
useIsConversationMode,
useIsWebhookMode,
} from './hooks/use-is-webhook';
import { useRunDataflow } from './hooks/use-run-dataflow';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { PipelineLogSheet } from './pipeline-log-sheet';
import PipelineRunSheet from './pipeline-run-sheet';
import { SettingDialog } from './setting-dialog';
import useGraphStore from './store';
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
}: ComponentPropsWithoutRef<typeof DropdownMenuItem>) {
return (
<DropdownMenuItem className="justify-start" {...props}>
{children}
</DropdownMenuItem>
);
}
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();
const { navigateToAgents } = useNavigatePage();
const {
visible: chatDrawerVisible,
hideModal: hideChatDrawer,
showModal: showChatDrawer,
} = useSetModalState();
const { t } = useTranslation();
useAgentHistoryManager();
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(() => {
if (inputs.length > 0) {
showChatDrawer();
} else {
handleRun();
}
}, [handleRun, inputs, showChatDrawer]);
const {
visible: versionDialogVisible,
hideModal: hideVersionDialog,
showModal: showVersionDialog,
} = useSetModalState();
const {
visible: settingDialogVisible,
hideModal: hideSettingDialog,
showModal: showSettingDialog,
} = useSetModalState();
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { navigateToAgentLogs, navigateToAgentExplore } = useNavigatePage();
const time = useWatchAgentChange(chatDrawerVisible);
const isWebhookMode = useIsWebhookMode();
const isConversationMode = useIsConversationMode();
// pipeline
const {
visible: pipelineRunSheetVisible,
hideModal: hidePipelineRunSheet,
showModal: showPipelineRunSheet,
} = useSetModalState();
const {
visible: webhookTestSheetVisible,
hideModal: hideWebhookTestSheet,
showModal: showWebhookTestSheet,
} = useSetModalState();
const {
visible: pipelineLogSheetVisible,
showModal: showPipelineLogSheet,
hideModal: hidePipelineLogSheet,
} = useSetModalState();
const {
visible: globalParamSheetVisible,
showModal: showGlobalParamSheet,
hideModal: hideGlobalParamSheet,
} = useSetModalState();
const {
isParsing,
logs,
messageId,
setMessageId,
isCompleted,
stopFetchTrace,
isLogEmpty,
} = useFetchPipelineLog(pipelineLogSheetVisible);
const findNodeByName = useGraphStore((state) => state.findNodeByName);
const handleRunPipeline = useCallback(() => {
if (!findNodeByName(Operator.Tokenizer)) {
message.warning(t('flow.tokenizerRequired'));
return;
}
if (isParsing) {
// show log sheet
showPipelineLogSheet();
} else {
hidePipelineLogSheet();
// handleRun();
showPipelineRunSheet();
}
}, [
findNodeByName,
hidePipelineLogSheet,
isParsing,
showPipelineLogSheet,
showPipelineRunSheet,
t,
]);
const { handleCancel } = useCancelCurrentDataflow({
messageId,
stopFetchTrace,
});
const handleButtonRunClick = useCallback(() => {
if (isWebhookMode) {
saveGraph();
showWebhookTestSheet();
} else if (isPipeline) {
handleRunPipeline();
} else {
handleRunAgent();
}
}, [
handleRunAgent,
handleRunPipeline,
isPipeline,
isWebhookMode,
saveGraph,
showWebhookTestSheet,
]);
const {
run: runPipeline,
loading: pipelineRunning,
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>
<section>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgents}>
{t('header.flow')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{agentDetail.title}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="text-xs text-text-secondary translate-y-3">
{t('flow.autosaved')} {time}
</div>
</section>
<div className="flex items-center gap-5">
<ButtonLoading
variant={'secondary'}
onClick={() => saveGraph()}
loading={loading}
>
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<Button
data-testid="agent-run"
variant={'secondary'}
onClick={handleButtonRunClick}
>
<CirclePlay />
{t('flow.run')}
</Button>
{isConversationMode && (
<Button
variant={'secondary'}
onClick={navigateToAgentExplore(id as string)}
>
<Compass />
{t('explore.title')}
</Button>
)}
<PublishConfirmDialog
agentDetail={agentDetail}
loading={loading}
onPublish={() => saveGraph(undefined, undefined, true)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'secondary'}>
<ChevronDown /> {t('flow.management')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<AgentDropdownMenuItem onClick={() => showGlobalParamSheet()}>
<MessageSquareCode />
{t('flow.conversationVariable')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showVersionDialog}>
<History />
{t('flow.historyVersion')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
{isPipeline || (
<AgentDropdownMenuItem
onClick={() => navigateToAgentLogs(id as string)()}
>
<Logs />
{t('flow.log')}
</AgentDropdownMenuItem>
)}
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={handleExportJson}>
<Upload />
{t('flow.export')}
</AgentDropdownMenuItem>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showSettingDialog}>
<Settings />
{t('flow.setting')}
</AgentDropdownMenuItem>
{isPipeline ||
(location.hostname !== 'cloud.ragflow.io' && (
<>
<DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showEmbedModal}>
<ScreenShare />
{t('common.embedIntoSite')}
</AgentDropdownMenuItem>
</>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</PageHeader>
<ReactFlowProvider>
<DropdownProvider>
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></AgentCanvas>
</DropdownProvider>
</ReactFlowProvider>
{embedVisible && (
<EmbedDialog
visible={embedVisible}
hideModal={hideEmbedModal}
token={id!}
from={SharedFrom.Agent}
beta={beta}
isAgent
initialWidgetSettings={initialWidgetSettings}
onSaveWidgetSettings={handleSaveWidgetSettings}
savingWidgetSettings={savingWidgetSettings}
></EmbedDialog>
)}
{versionDialogVisible && (
<DropdownProvider>
<VersionDialog hideModal={hideVersionDialog}></VersionDialog>
</DropdownProvider>
)}
{settingDialogVisible && (
<SettingDialog hideModal={hideSettingDialog}></SettingDialog>
)}
{pipelineLogSheetVisible && (
<PipelineLogSheet
hideModal={hidePipelineLogSheet}
isParsing={isParsing}
isCompleted={isCompleted}
isLogEmpty={isLogEmpty}
logs={logs}
handleCancel={handleCancel}
messageId={messageId}
uploadedFileData={uploadedFileData}
></PipelineLogSheet>
)}
{pipelineRunSheetVisible && (
<PipelineRunSheet
hideModal={hidePipelineRunSheet}
run={runPipeline}
loading={pipelineRunning}
></PipelineRunSheet>
)}
{globalParamSheetVisible && (
<GlobalParamSheet
data={{}}
hideModal={hideGlobalParamSheet}
></GlobalParamSheet>
)}
{webhookTestSheetVisible && (
<WebhookSheet hideModal={hideWebhookTestSheet}></WebhookSheet>
)}
</section>
);
}