mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Allow users to enter text in the middle of a chat #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -3,7 +3,14 @@ import { MessageType } from '@/constants/chat';
|
|||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
import { IReference, IReferenceChunk } from '@/interfaces/database/chat';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
import {
|
||||||
|
PropsWithChildren,
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useFetchDocumentInfosByIds,
|
useFetchDocumentInfosByIds,
|
||||||
@ -23,7 +30,10 @@ import styles from './index.less';
|
|||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {
|
interface IProps
|
||||||
|
extends Partial<IRemoveMessageById>,
|
||||||
|
IRegenerateMessage,
|
||||||
|
PropsWithChildren {
|
||||||
item: IMessage;
|
item: IMessage;
|
||||||
reference: IReference;
|
reference: IReference;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@ -52,6 +62,7 @@ const MessageItem = ({
|
|||||||
showLikeButton = true,
|
showLikeButton = true,
|
||||||
showLoudspeaker = true,
|
showLoudspeaker = true,
|
||||||
visibleAvatar = true,
|
visibleAvatar = true,
|
||||||
|
children,
|
||||||
}: IProps) => {
|
}: IProps) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const isAssistant = item.role === MessageType.Assistant;
|
const isAssistant = item.role === MessageType.Assistant;
|
||||||
@ -152,12 +163,16 @@ const MessageItem = ({
|
|||||||
: styles.messageUserText
|
: styles.messageUserText
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{item.data ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
content={item.content}
|
content={item.content}
|
||||||
reference={reference}
|
reference={reference}
|
||||||
clickDocumentButton={clickDocumentButton}
|
clickDocumentButton={clickDocumentButton}
|
||||||
></MarkdownContent>
|
></MarkdownContent>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isAssistant && referenceDocumentList.length > 0 && (
|
{isAssistant && referenceDocumentList.length > 0 && (
|
||||||
<List
|
<List
|
||||||
|
|||||||
@ -377,7 +377,7 @@ export const useSelectDerivedMessages = () => {
|
|||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
return pre.map((x) => {
|
return pre.map((x) => {
|
||||||
if (x.id === answer.id) {
|
if (x.id === answer.id) {
|
||||||
return { ...x, content: answer.answer };
|
return { ...x, ...answer, content: answer.answer };
|
||||||
}
|
}
|
||||||
return x;
|
return x;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Authorization } from '@/constants/authorization';
|
import { Authorization } from '@/constants/authorization';
|
||||||
|
import { BeginQuery } from '@/pages/agent/interface';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { getAuthorization } from '@/utils/authorization-util';
|
import { getAuthorization } from '@/utils/authorization-util';
|
||||||
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
||||||
@ -31,6 +32,12 @@ export interface INodeData {
|
|||||||
created_at: number;
|
created_at: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IInputData {
|
||||||
|
content: string;
|
||||||
|
inputs: Record<string, BeginQuery>;
|
||||||
|
tips: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IMessageData {
|
export interface IMessageData {
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
@ -39,6 +46,8 @@ export type INodeEvent = IAnswerEvent<INodeData>;
|
|||||||
|
|
||||||
export type IMessageEvent = IAnswerEvent<IMessageData>;
|
export type IMessageEvent = IAnswerEvent<IMessageData>;
|
||||||
|
|
||||||
|
export type IInputEvent = IAnswerEvent<IInputData>;
|
||||||
|
|
||||||
export type IChatEvent = INodeEvent | IMessageEvent;
|
export type IChatEvent = INodeEvent | IMessageEvent;
|
||||||
|
|
||||||
export type IEventList = Array<IChatEvent>;
|
export type IEventList = Array<IChatEvent>;
|
||||||
|
|||||||
@ -73,6 +73,7 @@ export interface Message {
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
audio_binary?: string;
|
audio_binary?: string;
|
||||||
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReferenceChunk {
|
export interface IReferenceChunk {
|
||||||
@ -102,6 +103,7 @@ export interface IAnswer {
|
|||||||
prompt?: string;
|
prompt?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
audio_binary?: string;
|
audio_binary?: string;
|
||||||
|
data?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Docagg {
|
export interface Docagg {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { IBeginNode } from '@/interfaces/database/flow';
|
import { IBeginNode } from '@/interfaces/database/flow';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { NodeProps, Position } from '@xyflow/react';
|
import { NodeProps, Position } from '@xyflow/react';
|
||||||
import { Flex } from 'antd';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -20,7 +20,7 @@ import { NodeWrapper } from './node-wrapper';
|
|||||||
// TODO: do not allow other nodes to connect to this node
|
// TODO: do not allow other nodes to connect to this node
|
||||||
function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) {
|
function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const query: BeginQuery[] = get(data, 'form.query', []);
|
const inputs: Record<string, BeginQuery> = get(data, 'form.inputs', {});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeWrapper>
|
<NodeWrapper>
|
||||||
@ -39,24 +39,22 @@ function InnerBeginNode({ data, id }: NodeProps<IBeginNode>) {
|
|||||||
{t(`flow.begin`)}
|
{t(`flow.begin`)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<Flex gap={8} vertical className={styles.generateParameters}>
|
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
|
||||||
{query.map((x, idx) => {
|
{Object.entries(inputs).map(([key, val], idx) => {
|
||||||
const Icon = BeginQueryTypeIconMap[x.type as BeginQueryType];
|
const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
|
||||||
return (
|
return (
|
||||||
<Flex
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
align="center"
|
className={cn(styles.conditionBlock, 'flex gap-1.5 items-center')}
|
||||||
gap={6}
|
|
||||||
className={styles.conditionBlock}
|
|
||||||
>
|
>
|
||||||
<Icon className="size-4" />
|
<Icon className="size-4" />
|
||||||
<label htmlFor="">{x.key}</label>
|
<label htmlFor="">{key}</label>
|
||||||
<span className={styles.parameterValue}>{x.name}</span>
|
<span className={styles.parameterValue}>{val.name}</span>
|
||||||
<span className="flex-1">{x.optional ? 'Yes' : 'No'}</span>
|
<span className="flex-1">{val.optional ? 'Yes' : 'No'}</span>
|
||||||
</Flex>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Flex>
|
</section>
|
||||||
</NodeWrapper>
|
</NodeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { useClickDrawer } from '@/components/pdf-drawer/hooks';
|
|||||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
|
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
|
||||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||||
|
import { InputForm } from './input-form';
|
||||||
|
|
||||||
const AgentChatBox = () => {
|
const AgentChatBox = () => {
|
||||||
const {
|
const {
|
||||||
@ -24,6 +25,7 @@ const AgentChatBox = () => {
|
|||||||
derivedMessages,
|
derivedMessages,
|
||||||
reference,
|
reference,
|
||||||
stopOutputMessage,
|
stopOutputMessage,
|
||||||
|
send,
|
||||||
} = useSendNextMessage();
|
} = useSendNextMessage();
|
||||||
|
|
||||||
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
||||||
@ -59,7 +61,9 @@ const AgentChatBox = () => {
|
|||||||
index={i}
|
index={i}
|
||||||
showLikeButton={false}
|
showLikeButton={false}
|
||||||
sendLoading={sendLoading}
|
sendLoading={sendLoading}
|
||||||
></MessageItem>
|
>
|
||||||
|
<InputForm send={send} message={message}></InputForm>
|
||||||
|
</MessageItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||||
import {
|
import {
|
||||||
IEventList,
|
IEventList,
|
||||||
|
IInputEvent,
|
||||||
IMessageEvent,
|
IMessageEvent,
|
||||||
MessageEventType,
|
MessageEventType,
|
||||||
useSendMessageBySSE,
|
useSendMessageBySSE,
|
||||||
@ -66,6 +67,21 @@ function findMessageFromList(eventList: IEventList) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findInputFromList(eventList: IEventList) {
|
||||||
|
const inputEvent = eventList.find(
|
||||||
|
(x) => x.event === MessageEventType.UserInputs,
|
||||||
|
) as IInputEvent;
|
||||||
|
|
||||||
|
if (!inputEvent) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: inputEvent?.message_id,
|
||||||
|
data: inputEvent?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const useGetBeginNodePrologue = () => {
|
const useGetBeginNodePrologue = () => {
|
||||||
const getNode = useGraphStore((state) => state.getNode);
|
const getNode = useGraphStore((state) => state.getNode);
|
||||||
|
|
||||||
@ -136,10 +152,12 @@ export const useSendNextMessage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { content, id } = findMessageFromList(answerList);
|
const { content, id } = findMessageFromList(answerList);
|
||||||
|
const inputAnswer = findInputFromList(answerList);
|
||||||
if (answerList.length > 0) {
|
if (answerList.length > 0) {
|
||||||
addNewestOneAnswer({
|
addNewestOneAnswer({
|
||||||
answer: content,
|
answer: content,
|
||||||
id: id,
|
id: id,
|
||||||
|
...inputAnswer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [answerList, addNewestOneAnswer]);
|
}, [answerList, addNewestOneAnswer]);
|
||||||
@ -181,5 +199,6 @@ export const useSendNextMessage = () => {
|
|||||||
ref,
|
ref,
|
||||||
removeMessageById,
|
removeMessageById,
|
||||||
stopOutputMessage,
|
stopOutputMessage,
|
||||||
|
send,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
86
web/src/pages/agent/chat/input-form.tsx
Normal file
86
web/src/pages/agent/chat/input-form.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Message } from '@/interfaces/database/chat';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useParams } from 'umi';
|
||||||
|
import { useSendNextMessage } from './hooks';
|
||||||
|
|
||||||
|
const FormSchema = z.object({
|
||||||
|
username: z.string().min(2, {
|
||||||
|
message: 'Username must be at least 2 characters.',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type InputFormProps = Pick<ReturnType<typeof useSendNextMessage>, 'send'> & {
|
||||||
|
message: Message;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InputForm({ send, message }: InputFormProps) {
|
||||||
|
const form = useForm<z.infer<typeof FormSchema>>({
|
||||||
|
resolver: zodResolver(FormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
username: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id: canvasId } = useParams();
|
||||||
|
|
||||||
|
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||||
|
const inputs = get(message, 'data.inputs', {});
|
||||||
|
|
||||||
|
const nextInputs = Object.entries(inputs).reduce((pre, [key, val]) => {
|
||||||
|
pre[key] = { ...val, value: data.username };
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
send({
|
||||||
|
inputs: nextInputs,
|
||||||
|
id: canvasId,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast('You submitted the following values', {
|
||||||
|
description: (
|
||||||
|
<pre className="mt-2 w-[320px] rounded-md bg-neutral-950 p-4">
|
||||||
|
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="shadcn" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Submit</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -23,10 +23,7 @@ export const useEditQueryRecord = ({
|
|||||||
const nextQuery: BeginQuery[] =
|
const nextQuery: BeginQuery[] =
|
||||||
index > -1 ? inputs.toSpliced(index, 1, record) : [...inputs, record];
|
index > -1 ? inputs.toSpliced(index, 1, record) : [...inputs, record];
|
||||||
|
|
||||||
form.setValue('inputs', nextQuery, {
|
form.setValue('inputs', nextQuery);
|
||||||
shouldDirty: true,
|
|
||||||
shouldTouch: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
hideModal();
|
hideModal();
|
||||||
},
|
},
|
||||||
@ -45,11 +42,11 @@ export const useEditQueryRecord = ({
|
|||||||
const handleDeleteRecord = useCallback(
|
const handleDeleteRecord = useCallback(
|
||||||
(idx: number) => {
|
(idx: number) => {
|
||||||
const inputs = form?.getValues('inputs') || [];
|
const inputs = form?.getValues('inputs') || [];
|
||||||
const nextQuery = inputs.filter(
|
const nextInputs = inputs.filter(
|
||||||
(item: BeginQuery, index: number) => index !== idx,
|
(item: BeginQuery, index: number) => index !== idx,
|
||||||
);
|
);
|
||||||
|
|
||||||
form.setValue('inputs', nextQuery, { shouldDirty: true });
|
form.setValue('inputs', nextInputs);
|
||||||
},
|
},
|
||||||
[form],
|
[form],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,8 +17,8 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
|||||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && form?.formState.isDirty) {
|
if (id) {
|
||||||
values = form?.getValues();
|
values = form?.getValues() || {};
|
||||||
|
|
||||||
const nextValues = {
|
const nextValues = {
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { UseFormReturn, useWatch } from 'react-hook-form';
|
|||||||
import useGraphStore from '../../store';
|
import useGraphStore from '../../store';
|
||||||
import { OutputArray, OutputObject } from './interface';
|
import { OutputArray, OutputObject } from './interface';
|
||||||
|
|
||||||
function transferToObject(list: OutputArray) {
|
export function transferToObject(list: OutputArray) {
|
||||||
return list.reduce<OutputObject>((pre, cur) => {
|
return list.reduce<OutputObject>((pre, cur) => {
|
||||||
pre[cur.name] = { ref: cur.ref, type: cur.type };
|
pre[cur.name] = { ref: cur.ref, type: cur.type };
|
||||||
return pre;
|
return pre;
|
||||||
|
|||||||
@ -17,8 +17,9 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
|||||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id && form?.formState.isDirty) {
|
// TODO: This should only be executed when the form changes
|
||||||
values = form?.getValues();
|
if (id) {
|
||||||
|
values = form?.getValues() || {};
|
||||||
|
|
||||||
const nextValues = {
|
const nextValues = {
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
Reference in New Issue
Block a user