Feat: Add a web search button to the chat box on the chat page. (#12786)

### What problem does this PR solve?

Feat: Add a web search button to the chat box on the chat page.

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2026-01-23 09:33:50 +08:00
committed by GitHub
parent e9453a3971
commit 8dd4a41bf8
12 changed files with 110 additions and 12 deletions

View File

@ -934,6 +934,7 @@ async def chatbots_inputs(dialog_id):
"title": dialog.name, "title": dialog.name,
"avatar": dialog.icon, "avatar": dialog.icon,
"prologue": dialog.prompt_config.get("prologue", ""), "prologue": dialog.prompt_config.get("prologue", ""),
"has_tavily_key": bool(dialog.prompt_config.get("tavily_api_key", "").strip()),
} }
) )

View File

@ -268,7 +268,7 @@ const FloatingChatWidget = () => {
// Wait for state to update, then send // Wait for state to update, then send
setTimeout(() => { setTimeout(() => {
handlePressEnter({ enableThinking: false }); handlePressEnter({ enableThinking: false, enableInternet: false });
// Clear our local input after sending // Clear our local input after sending
setInputValue(''); setInputValue('');
}, 50); }, 50);

View File

@ -16,7 +16,15 @@ import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { t } from 'i18next'; import { t } from 'i18next';
import { CircleStop, Paperclip, Send, Upload, X } from 'lucide-react'; import {
Atom,
CircleStop,
Globe,
Paperclip,
Send,
Upload,
X,
} from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -32,13 +40,20 @@ interface NextMessageInputProps {
isShared?: boolean; isShared?: boolean;
showUploadIcon?: boolean; showUploadIcon?: boolean;
isUploading?: boolean; isUploading?: boolean;
onPressEnter({ enableThinking }: { enableThinking: boolean }): void; onPressEnter({
enableThinking,
enableInternet,
}: {
enableThinking: boolean;
enableInternet: boolean;
}): void;
onInputChange: React.ChangeEventHandler<HTMLTextAreaElement>; onInputChange: React.ChangeEventHandler<HTMLTextAreaElement>;
createConversationBeforeUploadDocument?(message: string): Promise<any>; createConversationBeforeUploadDocument?(message: string): Promise<any>;
stopOutputMessage?(): void; stopOutputMessage?(): void;
onUpload?: NonNullable<FileUploadProps['onUpload']>; onUpload?: NonNullable<FileUploadProps['onUpload']>;
removeFile?(file: File): void; removeFile?(file: File): void;
showReasoning?: boolean; showReasoning?: boolean;
showInternet?: boolean;
} }
export type NextMessageInputOnPressEnterParameter = Parameters< export type NextMessageInputOnPressEnterParameter = Parameters<
@ -58,6 +73,7 @@ export function NextMessageInput({
onPressEnter, onPressEnter,
removeFile, removeFile,
showReasoning = false, showReasoning = false,
showInternet = false,
}: NextMessageInputProps) { }: NextMessageInputProps) {
const [files, setFiles] = React.useState<File[]>([]); const [files, setFiles] = React.useState<File[]>([]);
const [audioInputValue, setAudioInputValue] = React.useState<string | null>( const [audioInputValue, setAudioInputValue] = React.useState<string | null>(
@ -65,11 +81,23 @@ export function NextMessageInput({
); );
const [enableThinking, setEnableThinking] = useState(false); const [enableThinking, setEnableThinking] = useState(false);
const [enableInternet, setEnableInternet] = useState(false);
const handleThinkingToggle = useCallback(() => { const handleThinkingToggle = useCallback(() => {
setEnableThinking((prev) => !prev); setEnableThinking((prev) => !prev);
}, []); }, []);
const handleInternetToggle = useCallback(() => {
setEnableInternet((prev) => !prev);
}, []);
const pressEnter = useCallback(() => {
onPressEnter({
enableThinking,
enableInternet: showInternet ? enableInternet : false,
});
}, [onPressEnter, enableThinking, enableInternet, showInternet]);
useEffect(() => { useEffect(() => {
if (audioInputValue !== null) { if (audioInputValue !== null) {
onInputChange({ onInputChange({
@ -77,11 +105,19 @@ export function NextMessageInput({
} as React.ChangeEvent<HTMLTextAreaElement>); } as React.ChangeEvent<HTMLTextAreaElement>);
setTimeout(() => { setTimeout(() => {
onPressEnter({ enableThinking }); pressEnter();
setAudioInputValue(null); setAudioInputValue(null);
}, 0); }, 0);
} }
}, [audioInputValue, onInputChange, onPressEnter, enableThinking]); }, [
audioInputValue,
onInputChange,
onPressEnter,
enableThinking,
enableInternet,
showInternet,
pressEnter,
]);
const onFileReject = React.useCallback((file: File, message: string) => { const onFileReject = React.useCallback((file: File, message: string) => {
toast(message, { toast(message, {
@ -91,9 +127,9 @@ export function NextMessageInput({
const submit = React.useCallback(() => { const submit = React.useCallback(() => {
if (isUploading) return; if (isUploading) return;
onPressEnter({ enableThinking }); pressEnter();
setFiles([]); setFiles([]);
}, [isUploading, onPressEnter, enableThinking]); }, [isUploading, pressEnter]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
@ -205,9 +241,25 @@ export function NextMessageInput({
)} )}
onClick={handleThinkingToggle} onClick={handleThinkingToggle}
> >
<Atom />
<span>Thinking</span> <span>Thinking</span>
</Button> </Button>
)} )}
{showInternet && (
<Button
type="button"
variant="ghost"
className={cn(
'rounded-sm h-7 focus-visible:bg-none! hover:bg-none!',
{
'bg-accent-primary text-white': enableInternet,
},
)}
onClick={handleInternetToggle}
>
<Globe />
</Button>
)}
</div> </div>
{sendLoading ? ( {sendLoading ? (
<Button onClick={stopOutputMessage} className="size-5 rounded-sm"> <Button onClick={stopOutputMessage} className="size-5 rounded-sm">

View File

@ -13,6 +13,7 @@ export interface PromptConfig {
use_kg: boolean; use_kg: boolean;
reasoning?: boolean; reasoning?: boolean;
cross_languages?: Array<string>; cross_languages?: Array<string>;
tavily_api_key?: string;
} }
export interface Parameter { export interface Parameter {
@ -100,6 +101,7 @@ export interface Message {
chatBoxId?: string; chatBoxId?: string;
attachment?: IAttachment; attachment?: IAttachment;
reasoning?: boolean; reasoning?: boolean;
internet?: boolean;
} }
export interface IReferenceChunk { export interface IReferenceChunk {
@ -183,6 +185,7 @@ export interface IExternalChatInfo {
avatar?: string; avatar?: string;
title: string; title: string;
prologue?: string; prologue?: string;
has_tavily_key?: boolean;
} }
export interface IMessage extends Message { export interface IMessage extends Message {

View File

@ -291,6 +291,7 @@ export const useSendAgentMessage = ({
params.session_id = sessionId; params.session_id = sessionId;
params.reasoning = message.reasoning; params.reasoning = message.reasoning;
params.internet = message.internet;
} }
try { try {
@ -358,13 +359,21 @@ export const useSendAgentMessage = ({
]); ]);
const handlePressEnter = useCallback( const handlePressEnter = useCallback(
(...[{ enableThinking }]: NextMessageInputOnPressEnterParameter) => { (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
if (trim(value) === '') return; if (trim(value) === '') return;
const msgBody = buildRequestBody(value); const msgBody = buildRequestBody(value);
if (done) { if (done) {
setValue(''); setValue('');
sendMessage({ sendMessage({
message: { ...msgBody, reasoning: enableThinking }, message: {
...msgBody,
reasoning: enableThinking,
internet: enableInternet,
},
}); });
} }
addNewestOneQuestion({ ...msgBody, files: fileList }); addNewestOneQuestion({ ...msgBody, files: fileList });

View File

@ -39,6 +39,7 @@ import { useSendMessage } from '../../hooks/use-send-chat-message';
import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message'; import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message';
import { buildMessageItemReference } from '../../utils'; import { buildMessageItemReference } from '../../utils';
import { useAddChatBox } from '../use-add-box'; import { useAddChatBox } from '../use-add-box';
import { useShowInternet } from '../use-show-internet';
import { useSetDefaultModel } from './use-set-default-model'; import { useSetDefaultModel } from './use-set-default-model';
type MultipleChatBoxProps = { type MultipleChatBoxProps = {
@ -226,6 +227,8 @@ export function MultipleChatBox({
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
const showInternet = useShowInternet();
return ( return (
<section className="h-full flex flex-col px-5"> <section className="h-full flex flex-col px-5">
<div className="flex gap-4 flex-1 px-5 pb-14 min-h-0"> <div className="flex gap-4 flex-1 px-5 pb-14 min-h-0">
@ -261,6 +264,7 @@ export function MultipleChatBox({
stopOutputMessage={stopOutputMessage} stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile} onUpload={handleUploadFile}
showReasoning showReasoning
showInternet={showInternet}
/> />
</div> </div>
{visible && ( {visible && (

View File

@ -18,6 +18,7 @@ import {
import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation'; import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation';
import { useSendMessage } from '../../hooks/use-send-chat-message'; import { useSendMessage } from '../../hooks/use-send-chat-message';
import { buildMessageItemReference } from '../../utils'; import { buildMessageItemReference } from '../../utils';
import { useShowInternet } from '../use-show-internet';
interface IProps { interface IProps {
controller: AbortController; controller: AbortController;
@ -55,6 +56,8 @@ export function SingleChatBox({
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
const showInternet = useShowInternet();
useEffect(() => { useEffect(() => {
const messages = conversation?.message; const messages = conversation?.message;
if (Array.isArray(messages)) { if (Array.isArray(messages)) {
@ -120,6 +123,7 @@ export function SingleChatBox({
isUploading={isUploading} isUploading={isUploading}
removeFile={removeFile} removeFile={removeFile}
showReasoning showReasoning
showInternet={showInternet}
/> />
{visible && ( {visible && (
<PdfSheet <PdfSheet

View File

@ -0,0 +1,8 @@
import { useFetchDialog } from '@/hooks/use-chat-request';
import { isEmpty } from 'lodash';
export function useShowInternet() {
const { data: currentDialog } = useFetchDialog();
return !isEmpty(currentDialog?.prompt_config?.tavily_api_key);
}

View File

@ -135,7 +135,11 @@ export const useSendMessage = (controller: AbortController) => {
useCreateConversationBeforeSendMessage(); useCreateConversationBeforeSendMessage();
const handlePressEnter = useCallback( const handlePressEnter = useCallback(
async (...[{ enableThinking }]: NextMessageInputOnPressEnterParameter) => { async (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
if (trim(value) === '') return; if (trim(value) === '') return;
const data = await createConversationBeforeSendMessage(value); const data = await createConversationBeforeSendMessage(value);
@ -168,6 +172,7 @@ export const useSendMessage = (controller: AbortController) => {
files: files, files: files,
conversationId: targetConversationId, conversationId: targetConversationId,
reasoning: enableThinking, reasoning: enableThinking,
internet: enableInternet,
}, },
}); });
} }

View File

@ -177,7 +177,11 @@ export function useSendMultipleChatMessage(
); );
const handlePressEnter = useCallback( const handlePressEnter = useCallback(
async (...[{ enableThinking }]: NextMessageInputOnPressEnterParameter) => { async (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
if (trim(value) === '') return; if (trim(value) === '') return;
const id = uuid(); const id = uuid();
@ -214,6 +218,7 @@ export function useSendMultipleChatMessage(
files, files,
conversationId: targetConversationId, conversationId: targetConversationId,
reasoning: enableThinking, reasoning: enableThinking,
internet: enableInternet,
}, },
chatBoxId, chatBoxId,
currentConversationId: targetConversationId, currentConversationId: targetConversationId,

View File

@ -73,6 +73,7 @@ export const useSendSharedMessage = () => {
question: message.content, question: message.content,
session_id: get(derivedMessages, '0.session_id'), session_id: get(derivedMessages, '0.session_id'),
reasoning: message.reasoning, reasoning: message.reasoning,
internet: message.internet,
}); });
if (isCompletionError(res)) { if (isCompletionError(res)) {
@ -119,7 +120,11 @@ export const useSendSharedMessage = () => {
}, [answer, addNewestAnswer]); }, [answer, addNewestAnswer]);
const handlePressEnter = useCallback( const handlePressEnter = useCallback(
(...[{ enableThinking }]: NextMessageInputOnPressEnterParameter) => { (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
if (trim(value) === '') return; if (trim(value) === '') return;
const id = uuid(); const id = uuid();
if (done) { if (done) {
@ -135,6 +140,7 @@ export const useSendSharedMessage = () => {
id, id,
role: MessageType.User, role: MessageType.User,
reasoning: enableThinking, reasoning: enableThinking,
internet: enableInternet,
}); });
} }
}, },

View File

@ -124,6 +124,7 @@ const ChatContainer = () => {
showUploadIcon={false} showUploadIcon={false}
stopOutputMessage={stopOutputMessage} stopOutputMessage={stopOutputMessage}
showReasoning showReasoning
showInternet={chatInfo?.has_tavily_key}
></NextMessageInput> ></NextMessageInput>
</div> </div>
</div> </div>