Feat: Displays the embedded page of the chat module #3221 (#9532)

### What problem does this PR solve?

Feat: Displays the embedded page of the chat module #3221
Feat: Let the agen operator support the selection of tts model #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-08-18 18:02:13 +08:00
committed by GitHub
parent fe32952825
commit 9d0fed601d
21 changed files with 710 additions and 308 deletions

View File

@ -0,0 +1,48 @@
import { useFetchAppConf } from '@/hooks/logic-hooks';
import { RefreshCcw } from 'lucide-react';
import { PropsWithChildren } from 'react';
import { RAGFlowAvatar } from './ragflow-avatar';
import { Button } from './ui/button';
type EmbedContainerProps = {
title: string;
avatar?: string;
handleReset?(): void;
} & PropsWithChildren;
export function EmbedContainer({
title,
avatar,
children,
handleReset,
}: EmbedContainerProps) {
const appConf = useFetchAppConf();
return (
<section className="h-[100vh] flex justify-center items-center">
<div className="w-40 flex gap-2 absolute left-3 top-12 items-center">
<img src="/logo.svg" alt="" />
<span className="text-2xl font-bold">{appConf.appName}</span>
</div>
<div className=" w-[80vw] border rounded-lg">
<div className="flex justify-between items-center border-b p-3">
<div className="flex gap-2 items-center">
<RAGFlowAvatar avatar={avatar} name={title} isPerson />
<div className="text-xl text-foreground">{title}</div>
</div>
<Button
variant={'secondary'}
className="text-sm text-foreground cursor-pointer"
onClick={handleReset}
>
<div className="flex gap-1 items-center">
<RefreshCcw size={14} />
<span className="text-lg ">Reset</span>
</div>
</Button>
</div>
{children}
</div>
</section>
);
}

View File

@ -0,0 +1,171 @@
import CopyToClipboard from '@/components/copy-to-clipboard';
import HightLightMarkdown from '@/components/highlight-markdown';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Switch } from '@/components/ui/switch';
import { SharedFrom } from '@/constants/chat';
import {
LanguageAbbreviation,
LanguageAbbreviationMap,
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useCallback, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
const FormSchema = z.object({
visibleAvatar: z.boolean(),
locale: z.string(),
});
type IProps = IModalProps<any> & {
token: string;
from: SharedFrom;
beta: string;
isAgent: boolean;
};
function EmbedDialog({
hideModal,
token = '',
from,
beta = '',
isAgent,
}: IProps) {
const { t } = useTranslate('chat');
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
visibleAvatar: false,
locale: '',
},
});
const values = useWatch({ control: form.control });
const languageOptions = useMemo(() => {
return Object.values(LanguageAbbreviation).map((x) => ({
label: LanguageAbbreviationMap[x],
value: x,
}));
}, []);
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}`;
if (visibleAvatar) {
src += '&visible_avatar=1';
}
if (locale) {
src += `&locale=${locale}`;
}
return src;
}, [beta, from, token, values]);
const text = useMemo(() => {
const iframeSrc = generateIframeSrc();
return `
~~~ html
<iframe
src="${iframeSrc}"
style="width: 100%; height: 100%; min-height: 600px"
frameborder="0"
>
</iframe>
~~~
`;
}, [generateIframeSrc]);
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('embedIntoSite', { keyPrefix: 'common' })}
</DialogTitle>
</DialogHeader>
<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="visibleAvatar"
render={({ field }) => (
<FormItem>
<FormLabel>{t('avatarHidden')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="locale"
render={({ field }) => (
<FormItem>
<FormLabel>{t('locale')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={languageOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
<div>
<span>Embed code</span>
<HightLightMarkdown>{text}</HightLightMarkdown>
</div>
<div className=" font-medium mt-4 mb-1">
{t(isAgent ? 'flow' : 'chat', { keyPrefix: 'header' })}
<span className="ml-1 inline-block">ID</span>
</div>
<div className="bg-bg-card rounded-lg flex justify-between p-2">
<span>{token} </span>
<CopyToClipboard text={token}></CopyToClipboard>
</div>
<a
className="cursor-pointer text-accent-primary inline-block"
href={
isAgent
? 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-agent'
: 'https://ragflow.io/docs/dev/http_api_reference#create-session-with-chat-assistant'
}
target="_blank"
rel="noreferrer"
>
{t('howUseId', { keyPrefix: isAgent ? 'flow' : 'chat' })}
</a>
</section>
</DialogContent>
</Dialog>
);
}
export default memo(EmbedDialog);

View File

@ -0,0 +1,87 @@
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useFetchManualSystemTokenList } from '@/hooks/user-setting-hooks';
import { useCallback } from 'react';
import message from '../ui/message';
export const useShowTokenEmptyError = () => {
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
message.error(t('tokenError'));
}, [t]);
return { showTokenEmptyError };
};
export const useShowBetaEmptyError = () => {
const { t } = useTranslate('chat');
const showBetaEmptyError = useCallback(() => {
message.error(t('betaError'));
}, [t]);
return { showBetaEmptyError };
};
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
const { data: tokenList, fetchSystemTokenList } =
useFetchManualSystemTokenList();
let token = '',
beta = '';
if (Array.isArray(tokenList) && tokenList.length > 0) {
token = tokenList[0].token;
beta = tokenList[0].beta;
}
token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const ret = await fetchSystemTokenList();
const list = ret;
if (Array.isArray(list) && list.length > 0) {
if (!list[0].beta) {
showBetaEmptyError();
return false;
}
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]);
return {
token,
beta,
handleOperate,
};
};
export const useShowEmbedModal = () => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep();
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
beta,
};
};

View File

@ -16,7 +16,7 @@ import { Funnel } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { NextLLMSelect } from './llm-select/next';
import { NextInnerLLMSelectProps, NextLLMSelect } from './llm-select/next';
import { Button } from './ui/button';
const ModelTypes = [
@ -38,7 +38,10 @@ export const LargeModelFilterFormSchema = {
llm_filter: z.string().optional(),
};
export function LargeModelFormField() {
type LargeModelFormFieldProps = Pick<NextInnerLLMSelectProps, 'showTTSModel'>;
export function LargeModelFormField({
showTTSModel,
}: LargeModelFormFieldProps) {
const form = useFormContext();
const { t } = useTranslation();
const filter = useWatch({ control: form.control, name: 'llm_filter' });
@ -85,7 +88,11 @@ export function LargeModelFormField() {
/>
<FormControl>
<NextLLMSelect {...field} filter={filter} />
<NextLLMSelect
{...field}
filter={filter}
showTTSModel={showTTSModel}
/>
</FormControl>
</section>

View File

@ -1,29 +1,41 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { forwardRef, memo, useState } from 'react';
import { forwardRef, memo, useMemo, useState } from 'react';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
interface IProps {
export interface NextInnerLLMSelectProps {
id?: string;
value?: string;
onInitialValue?: (value: string, option: any) => void;
onChange?: (value: string) => void;
disabled?: boolean;
filter?: string;
showTTSModel?: boolean;
}
const NextInnerLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps
>(({ value, disabled, filter }, ref) => {
NextInnerLLMSelectProps
>(({ value, disabled, filter, showTTSModel = false }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelTypes =
filter === 'all' || filter === undefined
? [LlmModelType.Chat, LlmModelType.Image2text]
: [filter as LlmModelType];
const ttsModel = useMemo(() => {
return showTTSModel ? [LlmModelType.TTS] : [];
}, [showTTSModel]);
const modelTypes = useMemo(() => {
if (filter === LlmModelType.Chat) {
return [LlmModelType.Chat];
} else if (filter === LlmModelType.Image2text) {
return [LlmModelType.Image2text, ...ttsModel];
} else {
return [LlmModelType.Chat, LlmModelType.Image2text, ...ttsModel];
}
}, [filter, ttsModel]);
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
return (

View File

@ -14,6 +14,7 @@ import {
} from '@/components/file-upload';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CircleStop, Paperclip, Send, Upload, X } from 'lucide-react';
import * as React from 'react';
import { toast } from 'sonner';
@ -135,7 +136,11 @@ export function NextMessageInput({
disabled={isUploading || disabled || sendLoading}
onKeyDown={handleKeyDown}
/>
<div className="flex items-center justify-between gap-1.5">
<div
className={cn('flex items-center justify-between gap-1.5', {
'justify-end': !showUploadIcon,
})}
>
{showUploadIcon && (
<FileUploadTrigger asChild>
<Button