…be seen when selecting the next operator #3221 ### What problem does this PR solve? Fix: Fixed the issue that the content of the Dropdown section cannot be seen when selecting the next operator #3221 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.20.0
| @@ -1,7 +1,10 @@ | |||
| import classNames from 'classnames'; | |||
| import Markdown from 'react-markdown'; | |||
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; | |||
| import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; | |||
| import { | |||
| oneDark, | |||
| oneLight, | |||
| } from 'react-syntax-highlighter/dist/esm/styles/prism'; | |||
| import rehypeKatex from 'rehype-katex'; | |||
| import rehypeRaw from 'rehype-raw'; | |||
| import remarkGfm from 'remark-gfm'; | |||
| @@ -34,7 +37,7 @@ const HightLightMarkdown = ({ | |||
| {...rest} | |||
| PreTag="div" | |||
| language={match[1]} | |||
| style={dark && oneDark} | |||
| style={dark ? oneDark : oneLight} | |||
| > | |||
| {String(children).replace(/\n$/, '')} | |||
| </SyntaxHighlighter> | |||
| @@ -3,7 +3,6 @@ import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; | |||
| import * as SelectPrimitive from '@radix-ui/react-select'; | |||
| import { forwardRef, memo, useState } from 'react'; | |||
| import { LlmSettingFieldItems } from '../llm-setting-items/next'; | |||
| import { SelectWithSearch } from '../originui/select-with-search'; | |||
| import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; | |||
| import { Select, SelectTrigger, SelectValue } from '../ui/select'; | |||
| @@ -18,21 +17,13 @@ interface IProps { | |||
| const NextInnerLLMSelect = forwardRef< | |||
| React.ElementRef<typeof SelectPrimitive.Trigger>, | |||
| IProps | |||
| >(({ value, disabled, onChange }, ref) => { | |||
| >(({ value, disabled }, ref) => { | |||
| const [isPopoverOpen, setIsPopoverOpen] = useState(false); | |||
| const modelOptions = useComposeLlmOptionsByModelTypes([ | |||
| LlmModelType.Chat, | |||
| LlmModelType.Image2text, | |||
| ]); | |||
| return ( | |||
| <SelectWithSearch | |||
| options={modelOptions} | |||
| value={value} | |||
| onChange={onChange} | |||
| ></SelectWithSearch> | |||
| ); | |||
| return ( | |||
| <Select disabled={disabled} value={value}> | |||
| <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}> | |||
| @@ -5,6 +5,7 @@ import { camelCase } from 'lodash'; | |||
| import { useCallback } from 'react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { SelectWithSearch } from '../originui/select-with-search'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| @@ -15,9 +16,7 @@ import { | |||
| import { | |||
| Select, | |||
| SelectContent, | |||
| SelectGroup, | |||
| SelectItem, | |||
| SelectLabel, | |||
| SelectTrigger, | |||
| SelectValue, | |||
| } from '../ui/select'; | |||
| @@ -49,8 +48,6 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| LlmModelType.Image2text, | |||
| ]); | |||
| // useWatchFreedomChange(); | |||
| const handleChange = useHandleFreedomChange(); | |||
| const parameterOptions = Object.values(ModelVariableType).map((x) => ({ | |||
| @@ -74,7 +71,11 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| <FormItem> | |||
| <FormLabel>{t('model')}</FormLabel> | |||
| <FormControl> | |||
| <Select onValueChange={field.onChange} {...field}> | |||
| <SelectWithSearch | |||
| options={modelOptions} | |||
| {...field} | |||
| ></SelectWithSearch> | |||
| {/* <Select onValueChange={field.onChange} {...field}> | |||
| <SelectTrigger value={field.value}> | |||
| <SelectValue /> | |||
| </SelectTrigger> | |||
| @@ -94,7 +95,7 @@ export function LlmSettingFieldItems({ prefix }: LlmSettingFieldItemsProps) { | |||
| </SelectGroup> | |||
| ))} | |||
| </SelectContent> | |||
| </Select> | |||
| </Select> */} | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -32,6 +32,7 @@ import { RAGFlowSelectOptionType } from '../ui/select'; | |||
| export type SelectWithSearchFlagOptionType = { | |||
| label: ReactNode; | |||
| value?: string; | |||
| disabled?: boolean; | |||
| options?: RAGFlowSelectOptionType[]; | |||
| }; | |||
| @@ -119,6 +120,7 @@ export const SelectWithSearch = forwardRef< | |||
| <CommandItem | |||
| key={option.value} | |||
| value={option.value} | |||
| disabled={option.disabled} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none"> | |||
| @@ -138,6 +140,7 @@ export const SelectWithSearch = forwardRef< | |||
| <CommandItem | |||
| key={group.value} | |||
| value={group.value} | |||
| disabled={group.disabled} | |||
| onSelect={handleSelect} | |||
| > | |||
| <span className="text-lg leading-none">{group.label}</span> | |||
| @@ -2,6 +2,7 @@ import message from '@/components/ui/message'; | |||
| import { AgentGlobals } from '@/constants/agent'; | |||
| import { ITraceData } from '@/interfaces/database/agent'; | |||
| import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | |||
| import { IDebugSingleRequestBody } from '@/interfaces/request/agent'; | |||
| import i18n from '@/locales/config'; | |||
| import { BeginId } from '@/pages/agent/constant'; | |||
| import { useGetSharedChatSearchParams } from '@/pages/chat/shared-hooks'; | |||
| @@ -30,6 +31,8 @@ export const enum AgentApiAction { | |||
| UploadCanvasFile = 'uploadCanvasFile', | |||
| Trace = 'trace', | |||
| TestDbConnect = 'testDbConnect', | |||
| DebugSingle = 'debugSingle', | |||
| FetchInputForm = 'fetchInputForm', | |||
| } | |||
| export const EmptyDsl = { | |||
| @@ -353,3 +356,43 @@ export const useTestDbConnect = () => { | |||
| return { data, loading, testDbConnect: mutateAsync }; | |||
| }; | |||
| export const useDebugSingle = () => { | |||
| const { id } = useParams(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: [AgentApiAction.FetchInputForm], | |||
| mutationFn: async (params: IDebugSingleRequestBody) => { | |||
| const ret = await flowService.debugSingle({ id, ...params }); | |||
| if (ret?.data?.code !== 0) { | |||
| message.error(ret?.data?.message); | |||
| } | |||
| return ret?.data?.data; | |||
| }, | |||
| }); | |||
| return { data, loading, debugSingle: mutateAsync }; | |||
| }; | |||
| export const useFetchInputForm = (componentId?: string) => { | |||
| const { id } = useParams(); | |||
| const { data } = useQuery<Record<string, any>>({ | |||
| queryKey: [AgentApiAction.FetchInputForm], | |||
| initialData: {}, | |||
| enabled: !!id && !!componentId, | |||
| queryFn: async () => { | |||
| const { data } = await flowService.inputForm({ | |||
| id, | |||
| component_id: componentId, | |||
| }); | |||
| return data.data; | |||
| }, | |||
| }); | |||
| return data; | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export interface IDebugSingleRequestBody { | |||
| component_id: string; | |||
| params: Record<string, any>; | |||
| } | |||
| @@ -53,7 +53,7 @@ function AccordionOperators() { | |||
| return ( | |||
| <Accordion | |||
| type="multiple" | |||
| className="px-2 text-text-title" | |||
| className="px-2 text-text-title max-h-[45vh] overflow-auto" | |||
| defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']} | |||
| > | |||
| <AccordionItem value="item-1"> | |||
| @@ -21,10 +21,8 @@ export function InnerIterationNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<IIterationNode>) { | |||
| // const { theme } = useTheme(); | |||
| return ( | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <ToolBar selected={selected} id={id} label={data.label} showRun={false}> | |||
| <section | |||
| className={cn('h-full bg-transparent rounded-b-md ', { | |||
| [styles.selectedHeader]: selected, | |||
| @@ -56,9 +54,7 @@ export function InnerIterationNode({ | |||
| label={data.label} | |||
| wrapperClassName={cn( | |||
| 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]', | |||
| // styles.iterationHe ader, | |||
| { | |||
| // [`${styles.dark} text-white`]: theme === 'dark', | |||
| [styles.selectedHeader]: selected, | |||
| }, | |||
| )} | |||
| @@ -27,9 +27,16 @@ type ToolBarProps = { | |||
| selected?: boolean | undefined; | |||
| label: string; | |||
| id: string; | |||
| showRun?: boolean; | |||
| } & PropsWithChildren; | |||
| export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| export function ToolBar({ | |||
| selected, | |||
| children, | |||
| label, | |||
| id, | |||
| showRun = true, | |||
| }: ToolBarProps) { | |||
| const deleteNodeById = useGraphStore((store) => store.deleteNodeById); | |||
| const deleteIterationNodeById = useGraphStore( | |||
| (store) => store.deleteIterationNodeById, | |||
| @@ -63,9 +70,11 @@ export function ToolBar({ selected, children, label, id }: ToolBarProps) { | |||
| <TooltipContent position={Position.Top}> | |||
| <section className="flex gap-2 items-center"> | |||
| <IconWrapper> | |||
| <Play className="size-3.5" /> | |||
| </IconWrapper> | |||
| {showRun && ( | |||
| <IconWrapper> | |||
| <Play className="size-3.5" data-play /> | |||
| </IconWrapper> | |||
| )}{' '} | |||
| <IconWrapper onClick={handleDuplicate}> | |||
| <Copy className="size-3.5" /> | |||
| </IconWrapper> | |||
| @@ -320,6 +320,11 @@ export const initialCategorizeValues = { | |||
| parameter: ModelVariableType.Precise, | |||
| message_history_window_size: 1, | |||
| category_description: {}, | |||
| outputs: { | |||
| category_name: { | |||
| type: 'string', | |||
| }, | |||
| }, | |||
| }; | |||
| export const initialMessageValues = { | |||
| @@ -20,7 +20,7 @@ import OperatorIcon from '../operator-icon'; | |||
| import useGraphStore from '../store'; | |||
| import { needsSingleStepDebugging } from '../utils'; | |||
| import { FormConfigMap } from './form-config-map'; | |||
| import SingleDebugDrawer from './single-debug-drawer'; | |||
| import SingleDebugSheet from './single-debug-sheet'; | |||
| interface IProps { | |||
| node?: RAGFlowNodeType; | |||
| @@ -115,11 +115,11 @@ const FormSheet = ({ | |||
| </section> | |||
| </SheetContent> | |||
| {singleDebugDrawerVisible && ( | |||
| <SingleDebugDrawer | |||
| <SingleDebugSheet | |||
| visible={singleDebugDrawerVisible} | |||
| hideModal={hideSingleDebugDrawer} | |||
| componentId={node?.id} | |||
| ></SingleDebugDrawer> | |||
| ></SingleDebugSheet> | |||
| )} | |||
| </Sheet> | |||
| ); | |||
| @@ -1,81 +0,0 @@ | |||
| import CopyToClipboard from '@/components/copy-to-clipboard'; | |||
| import { useDebugSingle, useFetchInputElements } from '@/hooks/flow-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Drawer } from 'antd'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import JsonView from 'react18-json-view'; | |||
| import 'react18-json-view/src/style.css'; | |||
| import DebugContent from '../../debug-content'; | |||
| interface IProps { | |||
| componentId?: string; | |||
| } | |||
| const SingleDebugDrawer = ({ | |||
| componentId, | |||
| visible, | |||
| hideModal, | |||
| }: IModalProps<any> & IProps) => { | |||
| const { t } = useTranslation(); | |||
| const { data: list } = useFetchInputElements(componentId); | |||
| const { debugSingle, data, loading } = useDebugSingle(); | |||
| const onOk = useCallback( | |||
| (nextValues: any[]) => { | |||
| if (componentId) { | |||
| debugSingle({ component_id: componentId, params: nextValues }); | |||
| } | |||
| }, | |||
| [componentId, debugSingle], | |||
| ); | |||
| const content = JSON.stringify(data, null, 2); | |||
| return ( | |||
| <Drawer | |||
| title={ | |||
| <div className="flex justify-between"> | |||
| {t('flow.testRun')} | |||
| <CloseOutlined onClick={hideModal} /> | |||
| </div> | |||
| } | |||
| width={'100%'} | |||
| onClose={hideModal} | |||
| open={visible} | |||
| getContainer={false} | |||
| mask={false} | |||
| placement={'bottom'} | |||
| height={'95%'} | |||
| closeIcon={null} | |||
| > | |||
| <section className="overflow-y-auto"> | |||
| <DebugContent | |||
| parameters={list} | |||
| ok={onOk} | |||
| isNext={false} | |||
| loading={loading} | |||
| submitButtonDisabled={list.length === 0} | |||
| ></DebugContent> | |||
| {!isEmpty(data) ? ( | |||
| <div className="mt-4 rounded-md bg-slate-200 border border-neutral-200"> | |||
| <div className="flex justify-between p-2"> | |||
| <span>JSON</span> | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </div> | |||
| <JsonView | |||
| src={data} | |||
| displaySize | |||
| collapseStringsAfterLength={100000000000} | |||
| className="w-full h-[800px] break-words overflow-auto p-2 bg-slate-100" | |||
| /> | |||
| </div> | |||
| ) : null} | |||
| </section> | |||
| </Drawer> | |||
| ); | |||
| }; | |||
| export default SingleDebugDrawer; | |||
| @@ -0,0 +1,80 @@ | |||
| import CopyToClipboard from '@/components/copy-to-clipboard'; | |||
| import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet'; | |||
| import { useDebugSingle } from '@/hooks/flow-hooks'; | |||
| import { useFetchInputForm } from '@/hooks/use-agent-request'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { X } from 'lucide-react'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import JsonView from 'react18-json-view'; | |||
| import 'react18-json-view/src/style.css'; | |||
| import DebugContent from '../../debug-content'; | |||
| import { buildBeginInputListFromObject } from '../../form/begin-form/utils'; | |||
| interface IProps { | |||
| componentId?: string; | |||
| } | |||
| const SingleDebugSheet = ({ | |||
| componentId, | |||
| visible, | |||
| hideModal, | |||
| }: IModalProps<any> & IProps) => { | |||
| const { t } = useTranslation(); | |||
| const inputForm = useFetchInputForm(componentId); | |||
| const { debugSingle, data, loading } = useDebugSingle(); | |||
| const list = useMemo(() => { | |||
| return buildBeginInputListFromObject(inputForm); | |||
| }, [inputForm]); | |||
| const onOk = useCallback( | |||
| (nextValues: any[]) => { | |||
| if (componentId) { | |||
| debugSingle({ component_id: componentId, params: nextValues }); | |||
| } | |||
| }, | |||
| [componentId, debugSingle], | |||
| ); | |||
| const content = JSON.stringify(data, null, 2); | |||
| return ( | |||
| <Sheet open={visible} modal={false}> | |||
| <SheetContent className="top-20 p-0" closeIcon={false}> | |||
| <SheetHeader className="py-2 px-5"> | |||
| <div className="flex justify-between "> | |||
| {t('flow.testRun')} | |||
| <X onClick={hideModal} className="cursor-pointer" /> | |||
| </div> | |||
| </SheetHeader> | |||
| <section className="overflow-y-auto pt-4 px-5"> | |||
| <DebugContent | |||
| parameters={list} | |||
| ok={onOk} | |||
| isNext={false} | |||
| loading={loading} | |||
| submitButtonDisabled={list.length === 0} | |||
| ></DebugContent> | |||
| {!isEmpty(data) ? ( | |||
| <div className="mt-4 rounded-md bg-slate-200 border border-neutral-200"> | |||
| <div className="flex justify-between p-2"> | |||
| <span>JSON</span> | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </div> | |||
| <JsonView | |||
| src={data} | |||
| displaySize | |||
| collapseStringsAfterLength={100000000000} | |||
| className="w-full h-[800px] break-words overflow-auto p-2 bg-slate-100" | |||
| /> | |||
| </div> | |||
| ) : null} | |||
| </section> | |||
| </SheetContent> | |||
| </Sheet> | |||
| ); | |||
| }; | |||
| export default SingleDebugSheet; | |||
| @@ -28,8 +28,6 @@ import { | |||
| useState, | |||
| } from 'react'; | |||
| import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form'; | |||
| import { Operator } from '../../constant'; | |||
| import { useBuildFormSelectOptions } from '../../form-hooks'; | |||
| import DynamicExample from './dynamic-example'; | |||
| interface IProps { | |||
| @@ -107,13 +105,9 @@ const InnerNameInput = ({ | |||
| const NameInput = memo(InnerNameInput); | |||
| const InnerFormSet = ({ nodeId, index }: IProps & { index: number }) => { | |||
| const InnerFormSet = ({ index }: IProps & { index: number }) => { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslate('flow'); | |||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | |||
| Operator.Categorize, | |||
| nodeId, | |||
| ); | |||
| const buildFieldName = useCallback( | |||
| (name: string) => { | |||
| @@ -25,7 +25,7 @@ const DynamicExample = ({ name }: DynamicExampleProps) => { | |||
| return ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel> | |||
| <FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel> | |||
| <div className="space-y-4"> | |||
| {fields.map((field, index) => ( | |||
| <div key={field.id} className="flex items-start gap-2"> | |||
| @@ -8,12 +8,17 @@ import { memo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { initialCategorizeValues } from '../../constant'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { buildOutputList } from '../../utils/build-output-list'; | |||
| import { Output } from '../components/output'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import DynamicCategorize from './dynamic-categorize'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const outputList = buildOutputList(initialCategorizeValues.outputs); | |||
| function CategorizeForm({ node }: INextOperatorForm) { | |||
| const { t } = useTranslation(); | |||
| @@ -62,6 +67,7 @@ function CategorizeForm({ node }: INextOperatorForm) { | |||
| </FormContainer> | |||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||
| <DynamicCategorize nodeId={node?.id}></DynamicCategorize> | |||
| <Output list={outputList}></Output> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| @@ -17,6 +17,7 @@ import { | |||
| Download, | |||
| History, | |||
| Key, | |||
| LaptopMinimalCheck, | |||
| Logs, | |||
| ScreenShare, | |||
| Upload, | |||
| @@ -91,7 +92,7 @@ export default function Agent() { | |||
| onClick={() => saveGraph()} | |||
| loading={loading} | |||
| > | |||
| Save | |||
| <LaptopMinimalCheck /> Save | |||
| </ButtonLoading> | |||
| <Button variant={'secondary'} onClick={handleRunAgent}> | |||
| <CirclePlay /> | |||
| @@ -0,0 +1,149 @@ | |||
| import { MessageType, SharedFrom } from '@/constants/chat'; | |||
| import { useCreateNextSharedConversation } from '@/hooks/chat-hooks'; | |||
| import { | |||
| useHandleMessageInputChange, | |||
| useSelectDerivedMessages, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { Message } from '@/interfaces/database/chat'; | |||
| import { message } from 'antd'; | |||
| import { get } from 'lodash'; | |||
| import trim from 'lodash/trim'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useSearchParams } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| const isCompletionError = (res: any) => | |||
| res && (res?.response.status !== 200 || res?.data?.code !== 0); | |||
| export const useSendButtonDisabled = (value: string) => { | |||
| return trim(value) === ''; | |||
| }; | |||
| export const useGetSharedChatSearchParams = () => { | |||
| const [searchParams] = useSearchParams(); | |||
| const data_prefix = 'data_'; | |||
| const data = Object.fromEntries( | |||
| searchParams | |||
| .entries() | |||
| .filter(([key, value]) => key.startsWith(data_prefix)) | |||
| .map(([key, value]) => [key.replace(data_prefix, ''), value]), | |||
| ); | |||
| return { | |||
| from: searchParams.get('from') as SharedFrom, | |||
| sharedId: searchParams.get('shared_id'), | |||
| locale: searchParams.get('locale'), | |||
| data: data, | |||
| visibleAvatar: searchParams.get('visible_avatar') | |||
| ? searchParams.get('visible_avatar') !== '1' | |||
| : true, | |||
| }; | |||
| }; | |||
| export const useSendSharedMessage = () => { | |||
| const { | |||
| from, | |||
| sharedId: conversationId, | |||
| data: data, | |||
| } = useGetSharedChatSearchParams(); | |||
| const { createSharedConversation: setConversation } = | |||
| useCreateNextSharedConversation(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { send, answer, done, stopOutputMessage } = useSendMessageWithSse( | |||
| `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`, | |||
| ); | |||
| const { | |||
| derivedMessages, | |||
| ref, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| } = useSelectDerivedMessages(); | |||
| const [hasError, setHasError] = useState(false); | |||
| const sendMessage = useCallback( | |||
| async (message: Message, id?: string) => { | |||
| const res = await send({ | |||
| conversation_id: id ?? conversationId, | |||
| quote: true, | |||
| question: message.content, | |||
| session_id: get(derivedMessages, '0.session_id'), | |||
| }); | |||
| if (isCompletionError(res)) { | |||
| // cancel loading | |||
| setValue(message.content); | |||
| removeLatestMessage(); | |||
| } | |||
| }, | |||
| [send, conversationId, derivedMessages, setValue, removeLatestMessage], | |||
| ); | |||
| const handleSendMessage = useCallback( | |||
| async (message: Message) => { | |||
| if (conversationId !== '') { | |||
| sendMessage(message); | |||
| } else { | |||
| const data = await setConversation('user id'); | |||
| if (data.code === 0) { | |||
| const id = data.data.id; | |||
| sendMessage(message, id); | |||
| } | |||
| } | |||
| }, | |||
| [conversationId, setConversation, sendMessage], | |||
| ); | |||
| const fetchSessionId = useCallback(async () => { | |||
| const payload = { question: '' }; | |||
| const ret = await send({ ...payload, ...data }); | |||
| if (isCompletionError(ret)) { | |||
| message.error(ret?.data.message); | |||
| setHasError(true); | |||
| } | |||
| }, [data, send]); | |||
| useEffect(() => { | |||
| fetchSessionId(); | |||
| }, [fetchSessionId, send]); | |||
| useEffect(() => { | |||
| if (answer.answer) { | |||
| addNewestAnswer(answer); | |||
| } | |||
| }, [answer, addNewestAnswer]); | |||
| const handlePressEnter = useCallback( | |||
| (documentIds: string[]) => { | |||
| if (trim(value) === '') return; | |||
| const id = uuid(); | |||
| if (done) { | |||
| setValue(''); | |||
| addNewestQuestion({ | |||
| content: value, | |||
| doc_ids: documentIds, | |||
| id, | |||
| role: MessageType.User, | |||
| }); | |||
| handleSendMessage({ | |||
| content: value.trim(), | |||
| id, | |||
| role: MessageType.User, | |||
| }); | |||
| } | |||
| }, | |||
| [addNewestQuestion, done, handleSendMessage, setValue, value], | |||
| ); | |||
| return { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| sendLoading: !done, | |||
| ref, | |||
| loading: false, | |||
| derivedMessages, | |||
| hasError, | |||
| stopOutputMessage, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,13 @@ | |||
| .chatWrapper { | |||
| height: 100vh; | |||
| } | |||
| .chatContainer { | |||
| padding: 10px; | |||
| box-sizing: border-box; | |||
| height: 100%; | |||
| .messageContainer { | |||
| overflow-y: auto; | |||
| padding-right: 6px; | |||
| } | |||
| } | |||
| @@ -0,0 +1,13 @@ | |||
| import ChatContainer from './large'; | |||
| import styles from './index.less'; | |||
| const SharedChat = () => { | |||
| return ( | |||
| <div className={styles.chatWrapper}> | |||
| <ChatContainer></ChatContainer> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default SharedChat; | |||
| @@ -0,0 +1,123 @@ | |||
| import MessageInput from '@/components/message-input'; | |||
| import MessageItem from '@/components/message-item'; | |||
| import PdfDrawer from '@/components/pdf-drawer'; | |||
| import { useClickDrawer } from '@/components/pdf-drawer/hooks'; | |||
| import { MessageType, SharedFrom } from '@/constants/chat'; | |||
| import { useFetchNextConversationSSE } from '@/hooks/chat-hooks'; | |||
| import { useFetchFlowSSE } from '@/hooks/flow-hooks'; | |||
| import i18n from '@/locales/config'; | |||
| import { useSendButtonDisabled } from '@/pages/chat/hooks'; | |||
| import { buildMessageUuidWithRole } from '@/utils/chat'; | |||
| import { Flex, Spin } from 'antd'; | |||
| import React, { forwardRef, useMemo } from 'react'; | |||
| import { | |||
| useGetSharedChatSearchParams, | |||
| useSendSharedMessage, | |||
| } from '../hooks/use-send-shared-message'; | |||
| import { buildMessageItemReference } from '../utils'; | |||
| import styles from './index.less'; | |||
| const ChatContainer = () => { | |||
| const { | |||
| sharedId: conversationId, | |||
| from, | |||
| locale, | |||
| visibleAvatar, | |||
| } = useGetSharedChatSearchParams(); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| const { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| sendLoading, | |||
| loading, | |||
| ref, | |||
| derivedMessages, | |||
| hasError, | |||
| stopOutputMessage, | |||
| } = useSendSharedMessage(); | |||
| const sendDisabled = useSendButtonDisabled(value); | |||
| const useFetchAvatar = useMemo(() => { | |||
| return from === SharedFrom.Agent | |||
| ? useFetchFlowSSE | |||
| : useFetchNextConversationSSE; | |||
| }, [from]); | |||
| React.useEffect(() => { | |||
| if (locale && i18n.language !== locale) { | |||
| i18n.changeLanguage(locale); | |||
| } | |||
| }, [locale, visibleAvatar]); | |||
| const { data: avatarData } = useFetchAvatar(); | |||
| if (!conversationId) { | |||
| return <div>empty</div>; | |||
| } | |||
| return ( | |||
| <> | |||
| <Flex flex={1} className={styles.chatContainer} vertical> | |||
| <Flex flex={1} vertical className={styles.messageContainer}> | |||
| <div> | |||
| <Spin spinning={loading}> | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| <MessageItem | |||
| visibleAvatar={visibleAvatar} | |||
| key={buildMessageUuidWithRole(message)} | |||
| avatarDialog={avatarData?.avatar} | |||
| item={message} | |||
| nickname="You" | |||
| reference={buildMessageItemReference( | |||
| { | |||
| message: derivedMessages, | |||
| reference: [], | |||
| }, | |||
| message, | |||
| )} | |||
| loading={ | |||
| message.role === MessageType.Assistant && | |||
| sendLoading && | |||
| derivedMessages?.length - 1 === i | |||
| } | |||
| index={i} | |||
| clickDocumentButton={clickDocumentButton} | |||
| showLikeButton={false} | |||
| showLoudspeaker={false} | |||
| ></MessageItem> | |||
| ); | |||
| })} | |||
| </Spin> | |||
| </div> | |||
| <div ref={ref} /> | |||
| </Flex> | |||
| <MessageInput | |||
| isShared | |||
| value={value} | |||
| disabled={hasError} | |||
| sendDisabled={sendDisabled} | |||
| conversationId={conversationId} | |||
| onInputChange={handleInputChange} | |||
| onPressEnter={handlePressEnter} | |||
| sendLoading={sendLoading} | |||
| uploadMethod="external_upload_and_parse" | |||
| showUploadIcon={false} | |||
| stopOutputMessage={stopOutputMessage} | |||
| ></MessageInput> | |||
| </Flex> | |||
| {visible && ( | |||
| <PdfDrawer | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={documentId} | |||
| chunk={selectedChunk} | |||
| ></PdfDrawer> | |||
| )} | |||
| </> | |||
| ); | |||
| }; | |||
| export default forwardRef(ChatContainer); | |||
| @@ -0,0 +1,53 @@ | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { IConversation, IReference } from '@/interfaces/database/chat'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { EmptyConversationId } from './constants'; | |||
| import { IMessage } from './interface'; | |||
| export const isConversationIdExist = (conversationId: string) => { | |||
| return conversationId !== EmptyConversationId && conversationId !== ''; | |||
| }; | |||
| export const getDocumentIdsFromConversionReference = (data: IConversation) => { | |||
| const documentIds = data.reference.reduce( | |||
| (pre: Array<string>, cur: IReference) => { | |||
| cur.doc_aggs | |||
| ?.map((x) => x.doc_id) | |||
| .forEach((x) => { | |||
| if (pre.every((y) => y !== x)) { | |||
| pre.push(x); | |||
| } | |||
| }); | |||
| return pre; | |||
| }, | |||
| [], | |||
| ); | |||
| return documentIds.join(','); | |||
| }; | |||
| export const buildMessageItemReference = ( | |||
| conversation: { message: IMessage[]; reference: IReference[] }, | |||
| message: IMessage, | |||
| ) => { | |||
| const assistantMessages = conversation.message | |||
| ?.filter((x) => x.role === MessageType.Assistant) | |||
| .slice(1); | |||
| const referenceIndex = assistantMessages.findIndex( | |||
| (x) => x.id === message.id, | |||
| ); | |||
| const reference = !isEmpty(message?.reference) | |||
| ? message?.reference | |||
| : (conversation?.reference ?? [])[referenceIndex]; | |||
| return reference ?? { doc_aggs: [], chunks: [], total: 0 }; | |||
| }; | |||
| const oldReg = /(#{2}\d+\${2})/g; | |||
| export const currentReg = /\[ID:(\d+)\]/g; | |||
| // To be compatible with the old index matching mode | |||
| export const replaceTextByOldReg = (text: string) => { | |||
| return text?.replace(oldReg, (substring: string) => { | |||
| return `[ID:${substring.slice(2, -2)}]`; | |||
| }); | |||
| }; | |||
| @@ -20,6 +20,7 @@ const { | |||
| settingCanvas, | |||
| uploadCanvasFile, | |||
| trace, | |||
| inputForm, | |||
| } = api; | |||
| const methods = { | |||
| @@ -91,6 +92,10 @@ const methods = { | |||
| url: trace, | |||
| method: 'get', | |||
| }, | |||
| inputForm: { | |||
| url: inputForm, | |||
| method: 'get', | |||
| }, | |||
| } as const; | |||
| const flowService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -146,6 +146,8 @@ export default { | |||
| debug: `${api_host}/canvas/debug`, | |||
| uploadCanvasFile: `${api_host}/canvas/upload`, | |||
| trace: `${api_host}/canvas/trace`, | |||
| // agent | |||
| inputForm: `${api_host}/canvas/input_form`, | |||
| // mcp server | |||
| listMcpServer: `${api_host}/mcp_server/list`, | |||