feat(chat): 添加停止输出消息功能 (#12)

在聊天组件中添加了停止输出消息的功能,用户可以在消息生成过程中中断输出。同时,优化了搜索输入框的样式,隐藏了默认的搜索按钮,并替换为自定义的发送和停止按钮。

修改参照:https://github.com/infiniflow/ragflow/pull/6723/files
This commit is contained in:
zstar 2025-04-03 22:27:35 +08:00 committed by GitHub
parent a51b3168a0
commit 4f5be71eb3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 184 additions and 121 deletions

View File

@ -53,101 +53,101 @@ jobs:
version: ">=0.8.2" version: ">=0.8.2"
args: "check" args: "check"
- name: Build ragflow:nightly-slim # - name: Build ragflow:nightly-slim
run: | # run: |
RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-$HOME} # RUNNER_WORKSPACE_PREFIX=${RUNNER_WORKSPACE_PREFIX:-$HOME}
sudo docker pull ubuntu:22.04 # sudo docker pull ubuntu:22.04
sudo docker build --progress=plain --build-arg LIGHTEN=1 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim . # sudo docker build --progress=plain --build-arg LIGHTEN=1 --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly-slim .
- name: Build ragflow:nightly # - name: Build ragflow:nightly
run: | # run: |
sudo docker build --progress=plain --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly . # sudo docker build --progress=plain --build-arg NEED_MIRROR=1 -f Dockerfile -t infiniflow/ragflow:nightly .
- name: Start ragflow:nightly-slim # - name: Start ragflow:nightly-slim
run: | # run: |
echo -e "\nRAGFLOW_IMAGE=infiniflow/ragflow:nightly-slim" >> docker/.env # echo -e "\nRAGFLOW_IMAGE=infiniflow/ragflow:nightly-slim" >> docker/.env
sudo docker compose -f docker/docker-compose.yml up -d # sudo docker compose -f docker/docker-compose.yml up -d
- name: Stop ragflow:nightly-slim # - name: Stop ragflow:nightly-slim
if: always() # always run this step even if previous steps failed # if: always() # always run this step even if previous steps failed
run: | # run: |
sudo docker compose -f docker/docker-compose.yml down -v # sudo docker compose -f docker/docker-compose.yml down -v
- name: Start ragflow:nightly # - name: Start ragflow:nightly
run: | # run: |
echo -e "\nRAGFLOW_IMAGE=infiniflow/ragflow:nightly" >> docker/.env # echo -e "\nRAGFLOW_IMAGE=infiniflow/ragflow:nightly" >> docker/.env
sudo docker compose -f docker/docker-compose.yml up -d # sudo docker compose -f docker/docker-compose.yml up -d
- name: Run sdk tests against Elasticsearch # - name: Run sdk tests against Elasticsearch
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_sdk_api && pytest -s --tb=short get_email.py t_dataset.py t_chat.py t_session.py t_document.py t_chunk.py # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_sdk_api && pytest -s --tb=short get_email.py t_dataset.py t_chat.py t_session.py t_document.py t_chunk.py
- name: Run frontend api tests against Elasticsearch # - name: Run frontend api tests against Elasticsearch
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_frontend_api && pytest -s --tb=short get_email.py test_dataset.py # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_frontend_api && pytest -s --tb=short get_email.py test_dataset.py
- name: Run http api tests against Elasticsearch # - name: Run http api tests against Elasticsearch
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_http_api && pytest -s --tb=short -m "not slow" # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_http_api && pytest -s --tb=short -m "not slow"
- name: Stop ragflow:nightly # - name: Stop ragflow:nightly
if: always() # always run this step even if previous steps failed # if: always() # always run this step even if previous steps failed
run: | # run: |
sudo docker compose -f docker/docker-compose.yml down -v # sudo docker compose -f docker/docker-compose.yml down -v
- name: Start ragflow:nightly # - name: Start ragflow:nightly
run: | # run: |
sudo DOC_ENGINE=infinity docker compose -f docker/docker-compose.yml up -d # sudo DOC_ENGINE=infinity docker compose -f docker/docker-compose.yml up -d
- name: Run sdk tests against Infinity # - name: Run sdk tests against Infinity
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_sdk_api && pytest -s --tb=short get_email.py t_dataset.py t_chat.py t_session.py t_document.py t_chunk.py # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_sdk_api && pytest -s --tb=short get_email.py t_dataset.py t_chat.py t_session.py t_document.py t_chunk.py
- name: Run frontend api tests against Infinity # - name: Run frontend api tests against Infinity
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_frontend_api && pytest -s --tb=short get_email.py test_dataset.py # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_frontend_api && pytest -s --tb=short get_email.py test_dataset.py
- name: Run http api tests against Infinity # - name: Run http api tests against Infinity
run: | # run: |
export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY="" # export http_proxy=""; export https_proxy=""; export no_proxy=""; export HTTP_PROXY=""; export HTTPS_PROXY=""; export NO_PROXY=""
export HOST_ADDRESS=http://host.docker.internal:9380 # export HOST_ADDRESS=http://host.docker.internal:9380
until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do # until sudo docker exec ragflow-server curl -s --connect-timeout 5 ${HOST_ADDRESS} > /dev/null; do
echo "Waiting for service to be available..." # echo "Waiting for service to be available..."
sleep 5 # sleep 5
done # done
cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_http_api && DOC_ENGINE=infinity pytest -s --tb=short -m "not slow" # cd sdk/python && uv sync --python 3.10 --frozen && uv pip install . && source .venv/bin/activate && cd test/test_http_api && DOC_ENGINE=infinity pytest -s --tb=short -m "not slow"
- name: Stop ragflow:nightly # - name: Stop ragflow:nightly
if: always() # always run this step even if previous steps failed # if: always() # always run this step even if previous steps failed
run: | # run: |
sudo DOC_ENGINE=infinity docker compose -f docker/docker-compose.yml down -v # sudo DOC_ENGINE=infinity docker compose -f docker/docker-compose.yml down -v

View File

@ -128,6 +128,10 @@ This repository is available under the [Ragflow
- [v3-admin-vite](https://github.com/un-pany/v3-admin-vite) - [v3-admin-vite](https://github.com/un-pany/v3-admin-vite)
## 更新信息获取
主要更新日志会在我的微信公众号[我有一计]上发布,欢迎关注。
## Star History ## Star History
![Stargazers over time](https://starchart.cc/zstar1003/ragflow-plus.svg) ![Stargazers over time](https://starchart.cc/zstar1003/ragflow-plus.svg)

View File

@ -11,8 +11,6 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
InfoCircleOutlined, InfoCircleOutlined,
LoadingOutlined, LoadingOutlined,
PaperClipOutlined,
SendOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { GetProp, UploadFile } from 'antd'; import type { GetProp, UploadFile } from 'antd';
import { import {
@ -29,6 +27,7 @@ import {
UploadProps, UploadProps,
} from 'antd'; } from 'antd';
import get from 'lodash/get'; import get from 'lodash/get';
import { CircleStop, Paperclip, SendHorizontal } from 'lucide-react';
import { import {
ChangeEventHandler, ChangeEventHandler,
memo, memo,
@ -72,6 +71,7 @@ interface IProps {
isShared?: boolean; isShared?: boolean;
showUploadIcon?: boolean; showUploadIcon?: boolean;
createConversationBeforeUploadDocument?(message: string): Promise<any>; createConversationBeforeUploadDocument?(message: string): Promise<any>;
stopOutputMessage?(): void;
} }
const getBase64 = (file: FileType): Promise<string> => const getBase64 = (file: FileType): Promise<string> =>
@ -94,6 +94,7 @@ const MessageInput = ({
showUploadIcon = true, showUploadIcon = true,
createConversationBeforeUploadDocument, createConversationBeforeUploadDocument,
uploadMethod = 'upload_and_parse', uploadMethod = 'upload_and_parse',
stopOutputMessage,
}: IProps) => { }: IProps) => {
const { t } = useTranslate('chat'); const { t } = useTranslate('chat');
const { removeDocument } = useRemoveNextDocument(); const { removeDocument } = useRemoveNextDocument();
@ -150,6 +151,14 @@ const MessageInput = ({
const isUploadingFile = fileList.some((x) => x.status === 'uploading'); const isUploadingFile = fileList.some((x) => x.status === 'uploading');
const handlePressEnter = useCallback(async () => {
if (isUploadingFile) return;
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
onPressEnter(ids);
setFileList([]);
}, [fileList, onPressEnter, isUploadingFile]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
async (event: React.KeyboardEvent<HTMLTextAreaElement>) => { async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// check if it was shift + enter // check if it was shift + enter
@ -160,22 +169,9 @@ const MessageInput = ({
event.preventDefault(); event.preventDefault();
handlePressEnter(); handlePressEnter();
}, },
[fileList, onPressEnter, isUploadingFile], [sendDisabled, isUploadingFile, sendLoading, handlePressEnter],
); );
const handlePressEnter = useCallback(async () => {
if (isUploadingFile) return;
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
onPressEnter(ids);
setFileList([]);
}, [fileList, onPressEnter, isUploadingFile]);
const [isComposing, setIsComposing] = useState(false);
const handleCompositionStart = () => setIsComposing(true);
const handleCompositionEnd = () => setIsComposing(false);
const handleRemove = useCallback( const handleRemove = useCallback(
async (file: UploadFile) => { async (file: UploadFile) => {
const ids = get(file, 'response.data', []); const ids = get(file, 'response.data', []);
@ -199,6 +195,10 @@ const MessageInput = ({
[removeDocument, deleteDocument, isShared], [removeDocument, deleteDocument, isShared],
); );
const handleStopOutputMessage = useCallback(() => {
stopOutputMessage?.();
}, [stopOutputMessage]);
const getDocumentInfoById = useCallback( const getDocumentInfoById = useCallback(
(id: string) => { (id: string) => {
return documentInfos.find((x) => x.id === id); return documentInfos.find((x) => x.id === id);
@ -238,8 +238,6 @@ const MessageInput = ({
autoSize={{ minRows: 2, maxRows: 10 }} autoSize={{ minRows: 2, maxRows: 10 }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onChange={onInputChange} onChange={onInputChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
/> />
<Divider style={{ margin: '5px 30px 10px 0px' }} /> <Divider style={{ margin: '5px 30px 10px 0px' }} />
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
@ -342,18 +340,24 @@ const MessageInput = ({
}} }}
> >
<Button type={'primary'} disabled={disabled}> <Button type={'primary'} disabled={disabled}>
<PaperClipOutlined /> <Paperclip className="size-4" />
</Button> </Button>
</Upload> </Upload>
)} )}
<Button {sendLoading ? (
type="primary" <Button onClick={handleStopOutputMessage}>
onClick={handlePressEnter} <CircleStop className="size-5" />
loading={sendLoading} </Button>
disabled={sendDisabled || isUploadingFile || sendLoading} ) : (
> <Button
<SendOutlined /> type="primary"
</Button> onClick={handlePressEnter}
loading={sendLoading}
disabled={sendDisabled || isUploadingFile || sendLoading}
>
<SendHorizontal className="size-5" />
</Button>
)}
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -160,6 +160,11 @@ export const useSendMessageWithSse = (
const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); const [answer, setAnswer] = useState<IAnswer>({} as IAnswer);
const [done, setDone] = useState(true); const [done, setDone] = useState(true);
const timer = useRef<any>(); const timer = useRef<any>();
const sseRef = useRef<AbortController>();
const initializeSseRef = useCallback(() => {
sseRef.current = new AbortController();
}, []);
const resetAnswer = useCallback(() => { const resetAnswer = useCallback(() => {
if (timer.current) { if (timer.current) {
@ -176,6 +181,7 @@ export const useSendMessageWithSse = (
body: any, body: any,
controller?: AbortController, controller?: AbortController,
): Promise<{ response: Response; data: ResponseType } | undefined> => { ): Promise<{ response: Response; data: ResponseType } | undefined> => {
initializeSseRef();
try { try {
setDone(false); setDone(false);
const response = await fetch(url, { const response = await fetch(url, {
@ -185,7 +191,7 @@ export const useSendMessageWithSse = (
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
signal: controller?.signal, signal: controller?.signal || sseRef.current?.signal,
}); });
const res = response.clone().json(); const res = response.clone().json();
@ -230,10 +236,14 @@ export const useSendMessageWithSse = (
console.warn(e); console.warn(e);
} }
}, },
[url, resetAnswer], [initializeSseRef, url, resetAnswer],
); );
return { send, answer, done, setDone, resetAnswer }; const stopOutputMessage = useCallback(() => {
sseRef.current?.abort();
}, []);
return { send, answer, done, setDone, resetAnswer, stopOutputMessage };
}; };
export const useSpeechWithSse = (url: string = api.tts) => { export const useSpeechWithSse = (url: string = api.tts) => {

View File

@ -40,6 +40,7 @@ const ChatContainer = ({ controller }: IProps) => {
handlePressEnter, handlePressEnter,
regenerateMessage, regenerateMessage,
removeMessageById, removeMessageById,
stopOutputMessage,
} = useSendNextMessage(controller); } = useSendNextMessage(controller);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
@ -100,6 +101,7 @@ const ChatContainer = ({ controller }: IProps) => {
createConversationBeforeUploadDocument={ createConversationBeforeUploadDocument={
createConversationBeforeUploadDocument createConversationBeforeUploadDocument
} }
stopOutputMessage={stopOutputMessage}
></MessageInput> ></MessageInput>
</Flex> </Flex>
<PdfDrawer <PdfDrawer

View File

@ -375,6 +375,10 @@ export const useSendNextMessage = (controller: AbortController) => {
const { setConversationIsNew, getConversationIsNew } = const { setConversationIsNew, getConversationIsNew } =
useSetChatRouteParams(); useSetChatRouteParams();
const stopOutputMessage = useCallback(() => {
controller.abort();
}, [controller]);
const sendMessage = useCallback( const sendMessage = useCallback(
async ({ async ({
message, message,
@ -490,6 +494,7 @@ export const useSendNextMessage = (controller: AbortController) => {
ref, ref,
derivedMessages, derivedMessages,
removeMessageById, removeMessageById,
stopOutputMessage,
}; };
}; };

View File

@ -37,6 +37,7 @@ const ChatContainer = () => {
ref, ref,
derivedMessages, derivedMessages,
hasError, hasError,
stopOutputMessage,
} = useSendSharedMessage(); } = useSendSharedMessage();
const sendDisabled = useSendButtonDisabled(value); const sendDisabled = useSendButtonDisabled(value);
@ -105,6 +106,7 @@ const ChatContainer = () => {
sendLoading={sendLoading} sendLoading={sendLoading}
uploadMethod="external_upload_and_parse" uploadMethod="external_upload_and_parse"
showUploadIcon={false} showUploadIcon={false}
stopOutputMessage={stopOutputMessage}
></MessageInput> ></MessageInput>
</Flex> </Flex>
{visible && ( {visible && (

View File

@ -49,7 +49,7 @@ export const useSendSharedMessage = () => {
const { createSharedConversation: setConversation } = const { createSharedConversation: setConversation } =
useCreateNextSharedConversation(); useCreateNextSharedConversation();
const { handleInputChange, value, setValue } = useHandleMessageInputChange(); const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { send, answer, done } = useSendMessageWithSse( const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
`/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`, `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`,
); );
const { const {
@ -144,5 +144,6 @@ export const useSendSharedMessage = () => {
loading: false, loading: false,
derivedMessages, derivedMessages,
hasError, hasError,
stopOutputMessage,
}; };
}; };

View File

@ -24,6 +24,7 @@ const FlowChatBox = () => {
ref, ref,
derivedMessages, derivedMessages,
reference, reference,
stopOutputMessage,
} = useSendNextMessage(); } = useSendNextMessage();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
@ -75,6 +76,7 @@ const FlowChatBox = () => {
conversationId="" conversationId=""
onPressEnter={handlePressEnter} onPressEnter={handlePressEnter}
onInputChange={handleInputChange} onInputChange={handleInputChange}
stopOutputMessage={stopOutputMessage}
/> />
</Flex> </Flex>
<PdfDrawer <PdfDrawer

View File

@ -57,7 +57,9 @@ export const useSendNextMessage = () => {
const { handleInputChange, value, setValue } = useHandleMessageInputChange(); const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { refetch } = useFetchFlow(); const { refetch } = useFetchFlow();
const { send, answer, done } = useSendMessageWithSse(api.runCanvas); const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
api.runCanvas,
);
const sendMessage = useCallback( const sendMessage = useCallback(
async ({ message }: { message: Message; messages?: Message[] }) => { async ({ message }: { message: Message; messages?: Message[] }) => {
@ -134,5 +136,6 @@ export const useSendNextMessage = () => {
derivedMessages, derivedMessages,
ref, ref,
removeMessageById, removeMessageById,
stopOutputMessage,
}; };
}; };

View File

@ -17,7 +17,9 @@ import {
} from 'react'; } from 'react';
export const useSendQuestion = (kbIds: string[]) => { export const useSendQuestion = (kbIds: string[]) => {
const { send, answer, done } = useSendMessageWithSse(api.ask); const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
api.ask,
);
const { testChunk, loading } = useTestChunkRetrieval(); const { testChunk, loading } = useTestChunkRetrieval();
const [sendingLoading, setSendingLoading] = useState(false); const [sendingLoading, setSendingLoading] = useState(false);
const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer);
@ -116,6 +118,7 @@ export const useSendQuestion = (kbIds: string[]) => {
isFirstRender, isFirstRender,
selectedDocumentIds, selectedDocumentIds,
isSearchStrEmpty: isEmpty(trim(searchStr)), isSearchStrEmpty: isEmpty(trim(searchStr)),
stopOutputMessage,
}; };
}; };

View File

@ -137,6 +137,12 @@
.input(); .input();
} }
.searchInput {
:global(.ant-input-search-button) {
display: none;
}
}
.appIcon { .appIcon {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;

View File

@ -12,6 +12,7 @@ import {
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks'; import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import { IReference } from '@/interfaces/database/chat'; import { IReference } from '@/interfaces/database/chat';
import { import {
Button,
Card, Card,
Divider, Divider,
Flex, Flex,
@ -28,9 +29,11 @@ import {
Tag, Tag,
Tooltip, Tooltip,
} from 'antd'; } from 'antd';
import classNames from 'classnames';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { useMemo, useState } from 'react'; import { CircleStop, SendHorizontal } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import MarkdownContent from '../chat/markdown-content'; import MarkdownContent from '../chat/markdown-content';
import { useSendQuestion, useShowMindMapDrawer } from './hooks'; import { useSendQuestion, useShowMindMapDrawer } from './hooks';
@ -64,6 +67,7 @@ const SearchPage = () => {
isFirstRender, isFirstRender,
selectedDocumentIds, selectedDocumentIds,
isSearchStrEmpty, isSearchStrEmpty,
stopOutputMessage,
} = useSendQuestion(checkedWithoutEmbeddingIdList); } = useSendQuestion(checkedWithoutEmbeddingIdList);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
@ -81,18 +85,35 @@ const SearchPage = () => {
handleTestChunk(selectedDocumentIds, pageNumber, pageSize); handleTestChunk(selectedDocumentIds, pageNumber, pageSize);
}; };
const handleSearch = useCallback(() => {
sendQuestion(searchStr);
}, [searchStr, sendQuestion]);
const InputSearch = ( const InputSearch = (
<Search <Search
value={searchStr} value={searchStr}
onChange={handleSearchStrChange} onChange={handleSearchStrChange}
placeholder={t('header.search')} placeholder={t('header.search')}
allowClear allowClear
enterButton addonAfter={
sendingLoading ? (
<Button onClick={stopOutputMessage}>
<CircleStop />
</Button>
) : (
<Button onClick={handleSearch}>
<SendHorizontal className="size-5 text-blue-500" />
</Button>
)
}
onSearch={sendQuestion} onSearch={sendQuestion}
size="large" size="large"
loading={sendingLoading} loading={sendingLoading}
disabled={checkedWithoutEmbeddingIdList.length === 0} disabled={checkedWithoutEmbeddingIdList.length === 0}
className={isFirstRender ? styles.globalInput : styles.partialInput} className={classNames(
styles.searchInput,
isFirstRender ? styles.globalInput : styles.partialInput,
)}
/> />
); );