mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 07:01:04 +08:00
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>
421 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|