Feat: Users can chat directly without first creating a conversation. #11768 (#11769)

### What problem does this PR solve?

Feat: Users can chat directly without first creating a conversation.
#11768
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-05 17:34:41 +08:00
committed by GitHub
parent 885eb2eab9
commit ea38e12d42
20 changed files with 410 additions and 308 deletions

View File

@ -15,12 +15,12 @@ import {
import { MessageType } from '@/constants/chat';
import { useScrollToBottom } from '@/hooks/logic-hooks';
import {
useFetchConversation,
useFetchDialog,
useGetChatSearchParams,
useSetDialog,
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { IClientConversation, IMessage } from '@/interfaces/database/chat';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
@ -38,13 +38,14 @@ import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-creat
import { useSendMessage } from '../../hooks/use-send-chat-message';
import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message';
import { buildMessageItemReference } from '../../utils';
import { IMessage } from '../interface';
import { useAddChatBox } from '../use-add-box';
import { useSetDefaultModel } from './use-set-default-model';
type MultipleChatBoxProps = {
controller: AbortController;
chatBoxIds: string[];
stopOutputMessage(): void;
conversation: IClientConversation;
} & Pick<
ReturnType<typeof useAddChatBox>,
'removeChatBox' | 'addChatBox' | 'chatBoxIds'
@ -55,6 +56,7 @@ type ChatCardProps = {
idx: number;
derivedMessages: IMessage[];
sendLoading: boolean;
conversation: IClientConversation;
} & Pick<
MultipleChatBoxProps,
'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds'
@ -72,6 +74,7 @@ const ChatCard = forwardRef(function ChatCard(
derivedMessages,
sendLoading,
clickDocumentButton,
conversation,
}: ChatCardProps,
ref,
) {
@ -97,7 +100,8 @@ const ChatCard = forwardRef(function ChatCard(
const { data: userInfo } = useFetchUserInfo();
const { data: currentDialog } = useFetchDialog();
const { data: conversation } = useFetchConversation();
useSetDefaultModel(form);
const isLatestChat = idx === chatBoxIds.length - 1;
@ -202,6 +206,7 @@ export function MultipleChatBox({
removeChatBox,
addChatBox,
stopOutputMessage,
conversation,
}: MultipleChatBoxProps) {
const {
value,
@ -237,6 +242,7 @@ export function MultipleChatBox({
ref={setFormRef(id)}
sendLoading={sendLoading}
clickDocumentButton={clickDocumentButton}
conversation={conversation}
></ChatCard>
))}
</div>

View File

@ -4,12 +4,13 @@ import PdfSheet from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType } from '@/constants/chat';
import {
useFetchConversation,
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { useEffect } from 'react';
import {
useGetSendButtonDisabled,
useSendButtonDisabled,
@ -21,9 +22,14 @@ import { buildMessageItemReference } from '../../utils';
interface IProps {
controller: AbortController;
stopOutputMessage(): void;
conversation: IClientConversation;
}
export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
export function SingleChatBox({
controller,
stopOutputMessage,
conversation,
}: IProps) {
const {
value,
scrollRef,
@ -37,18 +43,32 @@ export function SingleChatBox({ controller, stopOutputMessage }: IProps) {
removeMessageById,
handleUploadFile,
removeFile,
setDerivedMessages,
} = useSendMessage(controller);
const { data: userInfo } = useFetchUserInfo();
const { data: currentDialog } = useFetchDialog();
const { createConversationBeforeUploadDocument } =
useCreateConversationBeforeUploadDocument();
const { conversationId } = useGetChatSearchParams();
const { data: conversation } = useFetchConversation();
const disabled = useGetSendButtonDisabled();
const sendDisabled = useSendButtonDisabled(value);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
useEffect(() => {
const messages = conversation?.message;
if (Array.isArray(messages)) {
setDerivedMessages(messages);
}
}, [conversation?.message, setDerivedMessages]);
useEffect(() => {
// Clear the message list after deleting the conversation.
if (conversationId === '') {
setDerivedMessages([]);
}
}, [conversationId, setDerivedMessages]);
return (
<section className="flex flex-col p-5 h-full">
<div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0">

View File

@ -0,0 +1,18 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { useMount } from 'ahooks';
import { UseFormReturn } from 'react-hook-form';
export function useSetDefaultModel(form: UseFormReturn<any>) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
useMount(() => {
const firstModel = modelOptions.at(0)?.options.at(0)?.value;
if (firstModel) {
form.setValue('llm_id', firstModel);
}
});
}

View File

@ -5,11 +5,15 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useRemoveConversation } from '@/hooks/use-chat-request';
import {
useGetChatSearchParams,
useRemoveConversation,
} from '@/hooks/use-chat-request';
import { IConversation } from '@/interfaces/database/chat';
import { Trash2 } from 'lucide-react';
import { MouseEventHandler, PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useChatUrlParams } from '../hooks/use-chat-url';
export function ConversationDropdown({
children,
@ -20,22 +24,27 @@ export function ConversationDropdown({
removeTemporaryConversation?: (conversationId: string) => void;
}) {
const { t } = useTranslation();
const { setConversationBoth } = useChatUrlParams();
const { removeConversation } = useRemoveConversation();
const { isNew } = useGetChatSearchParams();
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
if (conversation.is_new && removeTemporaryConversation) {
removeTemporaryConversation(conversation.id);
removeConversation([]);
} else {
removeConversation([conversation.id]);
}
}, [
conversation.id,
conversation.is_new,
removeConversation,
removeTemporaryConversation,
]);
const handleDelete: MouseEventHandler<HTMLDivElement> =
useCallback(async () => {
if (isNew === 'true' && removeTemporaryConversation) {
removeTemporaryConversation(conversation.id);
} else {
const code = await removeConversation([conversation.id]);
if (code === 0) {
setConversationBoth('', '');
}
}
}, [
conversation.id,
isNew,
removeConversation,
removeTemporaryConversation,
setConversationBoth,
]);
return (
<DropdownMenu>

View File

@ -15,13 +15,17 @@ import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import {
useFetchConversation,
useFetchConversationList,
useFetchConversationManually,
useFetchDialog,
useGetChatSearchParams,
} from '@/hooks/use-chat-request';
import { IClientConversation } from '@/interfaces/database/chat';
import { cn } from '@/lib/utils';
import { useMount } from 'ahooks';
import { isEmpty } from 'lodash';
import { ArrowUpRight, LogOut, Send } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
@ -37,26 +41,54 @@ export default function Chat() {
const { navigateToChatList } = useNavigatePage();
const { data } = useFetchDialog();
const { t } = useTranslation();
const { data: conversation } = useFetchConversation();
const [currentConversation, setCurrentConversation] =
useState<IClientConversation>({} as IClientConversation);
const { fetchConversationManually } = useFetchConversationManually();
const { handleConversationCardClick, controller, stopOutputMessage } =
useHandleClickConversationCard();
const { visible: settingVisible, switchVisible: switchSettingVisible } =
useSetModalState(true);
const {
removeChatBox,
addChatBox,
chatBoxIds,
hasSingleChatBox,
hasThreeChatBox,
} = useAddChatBox();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
const { removeChatBox, addChatBox, chatBoxIds, hasSingleChatBox } =
useAddChatBox(isDebugMode);
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { conversationId, isNew } = useGetChatSearchParams();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
const { data: dialogList } = useFetchConversationList();
const currentConversationName = useMemo(() => {
return dialogList.find((x) => x.id === conversationId)?.name;
}, [conversationId, dialogList]);
const fetchConversation: typeof handleConversationCardClick = useCallback(
async (conversationId, isNew) => {
if (conversationId && !isNew) {
const conversation = await fetchConversationManually(conversationId);
if (!isEmpty(conversation)) {
setCurrentConversation(conversation);
}
}
},
[fetchConversationManually],
);
const handleSessionClick: typeof handleConversationCardClick = useCallback(
(conversationId, isNew) => {
handleConversationCardClick(conversationId, isNew);
fetchConversation(conversationId, isNew);
},
[fetchConversation, handleConversationCardClick],
);
useMount(() => {
fetchConversation(conversationId, isNew === 'true');
});
if (isDebugMode) {
return (
@ -75,6 +107,7 @@ export default function Chat() {
removeChatBox={removeChatBox}
addChatBox={addChatBox}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
></MultipleChatBox>
</section>
);
@ -104,7 +137,7 @@ export default function Chat() {
<div className="flex flex-1 min-h-0 pb-9">
<Sessions
hasSingleChatBox={hasSingleChatBox}
handleConversationCardClick={handleConversationCardClick}
handleConversationCardClick={handleSessionClick}
switchSettingVisible={switchSettingVisible}
></Sessions>
@ -115,16 +148,8 @@ export default function Chat() {
className={cn('p-5', { 'border-b': hasSingleChatBox })}
>
<CardTitle className="flex justify-between items-center text-base">
<div className="truncate">{conversation.name}</div>
<Button
variant={'ghost'}
onClick={switchDebugMode}
disabled={
hasThreeChatBox ||
isEmpty(conversationId) ||
isNew === 'true'
}
>
<div className="truncate">{currentConversationName}</div>
<Button variant={'ghost'} onClick={switchDebugMode}>
<ArrowUpRight /> {t('chat.multipleModels')}
</Button>
</CardTitle>
@ -133,6 +158,7 @@ export default function Chat() {
<SingleChatBox
controller={controller}
stopOutputMessage={stopOutputMessage}
conversation={currentConversation}
></SingleChatBox>
</CardContent>
</Card>

View File

@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { v4 as uuid } from 'uuid';
export function useAddChatBox() {
export function useAddChatBox(isDebugMode: boolean) {
const [ids, setIds] = useState<string[]>([uuid()]);
const hasSingleChatBox = ids.length === 1;
@ -16,6 +16,12 @@ export function useAddChatBox() {
setIds((prev) => prev.filter((x) => x !== id));
}, []);
useEffect(() => {
if (!isDebugMode) {
setIds((pre) => pre.slice(0, 1));
}
}, [isDebugMode]);
return {
chatBoxIds: ids,
hasSingleChatBox,