Feat: Move the reasoning field to the root of the payload in the completion interface. (#12990)

### What problem does this PR solve?


### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2026-02-04 19:21:49 +08:00
committed by GitHub
parent 4d4b5a978d
commit 2627a7f5a8
13 changed files with 78 additions and 738 deletions

View File

@ -30,6 +30,11 @@ import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import { AudioButton } from '../ui/audio-button';
export type NextMessageInputOnPressEnterParameter = {
enableThinking: boolean;
enableInternet: boolean;
};
interface NextMessageInputProps {
disabled: boolean;
value: string;
@ -43,10 +48,7 @@ interface NextMessageInputProps {
onPressEnter({
enableThinking,
enableInternet,
}: {
enableThinking: boolean;
enableInternet: boolean;
}): void;
}: NextMessageInputOnPressEnterParameter): void;
onInputChange: React.ChangeEventHandler<HTMLTextAreaElement>;
createConversationBeforeUploadDocument?(message: string): Promise<any>;
stopOutputMessage?(): void;
@ -56,10 +58,6 @@ interface NextMessageInputProps {
showInternet?: boolean;
}
export type NextMessageInputOnPressEnterParameter = Parameters<
NextMessageInputProps['onPressEnter']
>;
export function NextMessageInput({
isUploading = false,
value,

View File

@ -1 +0,0 @@
export const ProgrammaticTag = 'programmatic';

View File

@ -1,76 +0,0 @@
.typeahead-popover {
background: #fff;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 8px;
position: fixed;
z-index: 1000;
}
.typeahead-popover ul {
list-style: none;
margin: 0;
max-height: 200px;
overflow-y: scroll;
}
.typeahead-popover ul::-webkit-scrollbar {
display: none;
}
.typeahead-popover ul {
-ms-overflow-style: none;
scrollbar-width: none;
}
.typeahead-popover ul li {
margin: 0;
min-width: 180px;
font-size: 14px;
outline: none;
cursor: pointer;
border-radius: 8px;
}
.typeahead-popover ul li.selected {
background: #eee;
}
.typeahead-popover li {
margin: 0 8px 0 8px;
color: #050505;
cursor: pointer;
line-height: 16px;
font-size: 15px;
display: flex;
align-content: center;
flex-direction: row;
flex-shrink: 0;
background-color: #fff;
border: 0;
}
.typeahead-popover li.active {
display: flex;
width: 20px;
height: 20px;
background-size: contain;
}
.typeahead-popover li .text {
display: flex;
line-height: 20px;
flex-grow: 1;
min-width: 150px;
}
.typeahead-popover li .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

View File

@ -1,160 +0,0 @@
import { CodeHighlightNode, CodeNode } from '@lexical/code';
import {
InitialConfigType,
LexicalComposer,
} from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import {
$getRoot,
$getSelection,
$nodesOfType,
EditorState,
Klass,
LexicalNode,
} from 'lexical';
import { cn } from '@/lib/utils';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { Variable } from 'lucide-react';
import { ReactNode, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
import theme from './theme';
import { VariableNode } from './variable-node';
import { VariableOnChangePlugin } from './variable-on-change-plugin';
import VariablePickerMenuPlugin from './variable-picker-plugin';
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error: Error) {
console.error(error);
}
const Nodes: Array<Klass<LexicalNode>> = [
HeadingNode,
QuoteNode,
CodeHighlightNode,
CodeNode,
VariableNode,
];
type PromptContentProps = { showToolbar?: boolean };
type IProps = {
value?: string;
onChange?: (value?: string) => void;
placeholder?: ReactNode;
} & PromptContentProps;
function PromptContent({ showToolbar = true }: PromptContentProps) {
const [editor] = useLexicalComposerContext();
const [isBlur, setIsBlur] = useState(false);
const { t } = useTranslation();
const insertTextAtCursor = useCallback(() => {
editor.update(() => {
const selection = $getSelection();
if (selection !== null) {
selection.insertText(' /');
}
});
}, [editor]);
const handleVariableIconClick = useCallback(() => {
insertTextAtCursor();
}, [insertTextAtCursor]);
const handleBlur = useCallback(() => {
setIsBlur(true);
}, []);
const handleFocus = useCallback(() => {
setIsBlur(false);
}, []);
return (
<section
className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })}
>
{showToolbar && (
<div className="border-b px-2 py-2 justify-end flex">
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm">
<Variable size={16} onClick={handleVariableIconClick} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t('flow.insertVariableTip')}</p>
</TooltipContent>
</Tooltip>
</div>
)}
<ContentEditable
className="min-h-40 relative px-2 py-1 focus-visible:outline-none"
onBlur={handleBlur}
onFocus={handleFocus}
/>
</section>
);
}
export function PromptEditor({
value,
onChange,
placeholder,
showToolbar,
}: IProps) {
const { t } = useTranslation();
const initialConfig: InitialConfigType = {
namespace: 'PromptEditor',
theme,
onError,
nodes: Nodes,
};
const onValueChange = useCallback(
(editorState: EditorState) => {
editorState?.read(() => {
const listNodes = $nodesOfType(VariableNode); // to be removed
// const allNodes = $dfs();
console.log('🚀 ~ onChange ~ allNodes:', listNodes);
const text = $getRoot().getTextContent();
onChange?.(text);
});
},
[onChange],
);
return (
<div className="relative">
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={
<PromptContent showToolbar={showToolbar}></PromptContent>
}
placeholder={
<div
className="absolute top-10 left-2 text-text-secondary"
data-xxx
>
{placeholder || t('common.pleaseInput')}
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
<VariableOnChangePlugin
onChange={onValueChange}
></VariableOnChangePlugin>
</LexicalComposer>
</div>
);
}

View File

@ -1,43 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export default {
code: 'editor-code',
heading: {
h1: 'editor-heading-h1',
h2: 'editor-heading-h2',
h3: 'editor-heading-h3',
h4: 'editor-heading-h4',
h5: 'editor-heading-h5',
},
image: 'editor-image',
link: 'editor-link',
list: {
listitem: 'editor-listitem',
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
},
ltr: 'ltr',
paragraph: 'editor-paragraph',
placeholder: 'editor-placeholder',
quote: 'editor-quote',
rtl: 'rtl',
text: {
bold: 'editor-text-bold',
code: 'editor-text-code',
hashtag: 'editor-text-hashtag',
italic: 'editor-text-italic',
overflowed: 'editor-text-overflowed',
strikethrough: 'editor-text-strikethrough',
underline: 'editor-text-underline',
underlineStrikethrough: 'editor-text-underlineStrikethrough',
},
};

View File

@ -1,70 +0,0 @@
import i18n from '@/locales/config';
import { BeginId } from '@/pages/agent/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react';
const prefix = BeginId + '@';
export class VariableNode extends DecoratorNode<ReactNode> {
__value: string;
__label: string;
static getType(): string {
return 'variable';
}
static clone(node: VariableNode): VariableNode {
return new VariableNode(node.__value, node.__label, node.__key);
}
constructor(value: string, label: string, key?: NodeKey) {
super(key);
this.__value = value;
this.__label = label;
}
createDOM(): HTMLElement {
const dom = document.createElement('span');
dom.className = 'mr-1';
return dom;
}
updateDOM(): false {
return false;
}
decorate(): ReactNode {
let content: ReactNode = (
<span className="text-blue-600">{this.__label}</span>
);
if (this.__value.startsWith(prefix)) {
content = (
<div>
<span>{i18n.t(`flow.begin`)}</span> / {content}
</div>
);
}
return (
<div className="bg-gray-200 dark:bg-gray-400 text-primary inline-flex items-center rounded-md px-2 py-0">
{content}
</div>
);
}
getTextContent(): string {
return `{${this.__value}}`;
}
}
export function $createVariableNode(
value: string,
label: string,
): VariableNode {
return new VariableNode(value, label);
}
export function $isVariableNode(
node: LexicalNode | null | undefined,
): node is VariableNode {
return node instanceof VariableNode;
}

View File

@ -1,35 +0,0 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { EditorState, LexicalEditor } from 'lexical';
import { useEffect } from 'react';
import { ProgrammaticTag } from './constant';
interface IProps {
onChange: (
editorState: EditorState,
editor?: LexicalEditor,
tags?: Set<string>,
) => void;
}
export function VariableOnChangePlugin({ onChange }: IProps) {
// Access the editor through the LexicalComposerContext
const [editor] = useLexicalComposerContext();
// Wrap our listener in useEffect to handle the teardown and avoid stale references.
useEffect(() => {
// most listeners return a teardown function that can be called to clean them up.
return editor.registerUpdateListener(
({ editorState, tags, dirtyElements }) => {
// Check if there is a "programmatic" tag
const isProgrammaticUpdate = tags.has(ProgrammaticTag);
// The onchange event is only triggered when the data is manually updated
// Otherwise, the content will be displayed incorrectly.
if (dirtyElements.size > 0 && !isProgrammaticUpdate) {
onChange(editorState);
}
},
);
}, [editor, onChange]);
return null;
}

View File

@ -1,273 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
useBasicTypeaheadTriggerMatch,
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
import {
$createParagraphNode,
$createTextNode,
$getRoot,
$getSelection,
$isRangeSelection,
TextNode,
} from 'lexical';
import React, {
ReactElement,
useCallback,
useContext,
useEffect,
useRef,
} from 'react';
import * as ReactDOM from 'react-dom';
import { FlowFormContext } from '@/pages/flow/context';
import { useBuildComponentIdSelectOptions } from '@/pages/flow/hooks/use-get-begin-query';
import { $createVariableNode } from './variable-node';
import { ProgrammaticTag } from './constant';
import './index.css';
class VariableInnerOption extends MenuOption {
label: string;
value: string;
constructor(label: string, value: string) {
super(value);
this.label = label;
this.value = value;
}
}
class VariableOption extends MenuOption {
label: ReactElement | string;
title: string;
options: VariableInnerOption[];
constructor(
label: ReactElement | string,
title: string,
options: VariableInnerOption[],
) {
super(title);
this.label = label;
this.title = title;
this.options = options;
}
}
function VariablePickerMenuItem({
index,
option,
selectOptionAndCleanUp,
}: {
index: number;
option: VariableOption;
selectOptionAndCleanUp: (
option: VariableOption | VariableInnerOption,
) => void;
}) {
return (
<li
key={option.key}
tabIndex={-1}
ref={option.setRefElement}
role="option"
id={'typeahead-item-' + index}
>
<div>
<span className="text text-slate-500">{option.title}</span>
<ul className="pl-2 py-1">
{option.options.map((x) => (
<li
key={x.value}
onClick={() => selectOptionAndCleanUp(x)}
className="hover:bg-slate-300 p-1"
>
{x.label}
</li>
))}
</ul>
</div>
</li>
);
}
export default function VariablePickerMenuPlugin({
value,
}: {
value?: string;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const isFirstRender = useRef(true);
const node = useContext(FlowFormContext);
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
});
const [queryString, setQueryString] = React.useState<string | null>('');
const options = useBuildComponentIdSelectOptions(node?.id, node?.parentId);
const filteredOptions = React.useMemo(() => {
if (!queryString) return options;
const lowerQuery = queryString.toLowerCase();
return options
.map((x) => ({
...x,
options: x.options.filter(
(y) =>
y.label.toLowerCase().includes(lowerQuery) ||
y.value.toLowerCase().includes(lowerQuery),
),
}))
.filter((x) => x.options.length > 0);
}, [options, queryString]);
const nextOptions: VariableOption[] = filteredOptions.map(
(x) =>
new VariableOption(
x.label,
x.title,
x.options.map((y) => new VariableInnerOption(y.label, y.value)),
),
);
const findLabelByValue = useCallback(
(value: string) => {
const children = options.reduce<Array<{ label: string; value: string }>>(
(pre, cur) => {
return pre.concat(cur.options);
},
[],
);
return children.find((x) => x.value === value)?.label;
},
[options],
);
const onSelectOption = useCallback(
(
selectedOption: VariableOption | VariableInnerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
) => {
editor.update(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection) || selectedOption === null) {
return;
}
if (nodeToRemove) {
nodeToRemove.remove();
}
selection.insertNodes([
$createVariableNode(
(selectedOption as VariableInnerOption).value,
selectedOption.label as string,
),
]);
closeMenu();
});
},
[editor],
);
const parseTextToVariableNodes = useCallback(
(text: string) => {
const paragraph = $createParagraphNode();
// Regular expression to match content within {}
const regex = /{([^}]*)}/g;
let match;
let lastIndex = 0;
while ((match = regex.exec(text)) !== null) {
const { 1: content, index, 0: template } = match;
// Add the previous text part (if any)
if (index > lastIndex) {
const textNode = $createTextNode(text.slice(lastIndex, index));
paragraph.append(textNode);
}
// Add variable node or text node
const label = findLabelByValue(content);
if (label) {
paragraph.append($createVariableNode(content, label));
} else {
paragraph.append($createTextNode(template));
}
// Update index
lastIndex = regex.lastIndex;
}
// Add the last part of text (if any)
if (lastIndex < text.length) {
const textNode = $createTextNode(text.slice(lastIndex));
paragraph.append(textNode);
}
$getRoot().clear().append(paragraph);
if ($isRangeSelection($getSelection())) {
$getRoot().selectEnd();
}
},
[findLabelByValue],
);
useEffect(() => {
if (editor && value && isFirstRender.current) {
isFirstRender.current = false;
editor.update(
() => {
parseTextToVariableNodes(value);
},
{ tag: ProgrammaticTag },
);
}
}, [parseTextToVariableNodes, editor, value]);
return (
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={nextOptions}
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) =>
anchorElementRef.current && options.length
? ReactDOM.createPortal(
<div className="typeahead-popover w-[200px] p-2">
<ul>
{nextOptions.map((option, i: number) => (
<VariablePickerMenuItem
index={i}
key={option.key}
option={option}
selectOptionAndCleanUp={selectOptionAndCleanUp}
/>
))}
</ul>
</div>,
anchorElementRef.current,
)
: null
}
/>
);
}

View File

@ -100,8 +100,6 @@ export interface Message {
files?: (File | UploadResponseDataType)[];
chatBoxId?: string;
attachment?: IAttachment;
reasoning?: boolean;
internet?: boolean;
}
export interface IReferenceChunk {

View File

@ -1,4 +1,3 @@
import { NextMessageInputOnPressEnterParameter } from '@/components/message-input/next';
import sonnerMessage from '@/components/ui/message';
import { MessageType } from '@/constants/chat';
import {
@ -290,8 +289,6 @@ export const useSendAgentMessage = ({
params.files = uploadResponseList;
params.session_id = sessionId;
params.reasoning = message.reasoning;
params.internet = message.internet;
}
try {
@ -358,39 +355,28 @@ export const useSendAgentMessage = ({
removeAllMessagesExceptFirst,
]);
const handlePressEnter = useCallback(
(
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
if (trim(value) === '') return;
const msgBody = buildRequestBody(value);
if (done) {
setValue('');
sendMessage({
message: {
...msgBody,
reasoning: enableThinking,
internet: enableInternet,
},
});
}
addNewestOneQuestion({ ...msgBody, files: fileList });
setTimeout(() => {
scrollToBottom();
}, 100);
},
[
value,
done,
addNewestOneQuestion,
fileList,
setValue,
sendMessage,
scrollToBottom,
],
);
const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const msgBody = buildRequestBody(value);
if (done) {
setValue('');
sendMessage({
message: msgBody,
});
}
addNewestOneQuestion({ ...msgBody, files: fileList });
setTimeout(() => {
scrollToBottom();
}, 100);
}, [
value,
done,
addNewestOneQuestion,
fileList,
setValue,
sendMessage,
scrollToBottom,
]);
const sendedTaskMessage = useRef<boolean>(false);

View File

@ -90,11 +90,13 @@ export const useSendMessage = (controller: AbortController) => {
message,
currentConversationId,
messages,
enableInternet,
enableThinking,
}: {
message: IMessage;
currentConversationId?: string;
messages?: IMessage[];
}) => {
} & NextMessageInputOnPressEnterParameter) => {
const res = await send(
{
conversation_id: currentConversationId ?? conversationId,
@ -104,6 +106,8 @@ export const useSendMessage = (controller: AbortController) => {
: (derivedMessages ?? [])),
message,
],
reasoning: enableThinking,
internet: enableInternet,
},
controller,
);
@ -135,11 +139,10 @@ export const useSendMessage = (controller: AbortController) => {
useCreateConversationBeforeSendMessage();
const handlePressEnter = useCallback(
async (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
async ({
enableThinking,
enableInternet,
}: NextMessageInputOnPressEnterParameter) => {
if (trim(value) === '') return;
const data = await createConversationBeforeSendMessage(value);
@ -171,9 +174,9 @@ export const useSendMessage = (controller: AbortController) => {
role: MessageType.User,
files: files,
conversationId: targetConversationId,
reasoning: enableThinking,
internet: enableInternet,
},
enableInternet,
enableThinking,
});
}
clearFiles();

View File

@ -138,12 +138,14 @@ export function useSendMultipleChatMessage(
currentConversationId,
messages,
chatBoxId,
enableInternet,
enableThinking,
}: {
message: Message;
currentConversationId?: string;
chatBoxId: string;
messages?: Message[];
}) => {
} & NextMessageInputOnPressEnterParameter) => {
let derivedMessages: IMessage[] = [];
derivedMessages = messageRecord[chatBoxId];
@ -153,6 +155,8 @@ export function useSendMultipleChatMessage(
chatBoxId,
conversation_id: currentConversationId ?? conversationId,
messages: [...(messages ?? derivedMessages ?? []), message],
reasoning: enableThinking,
internet: enableInternet,
...getLLMConfigById(chatBoxId),
},
controller,
@ -177,11 +181,10 @@ export function useSendMultipleChatMessage(
);
const handlePressEnter = useCallback(
async (
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
async ({
enableThinking,
enableInternet,
}: NextMessageInputOnPressEnterParameter) => {
if (trim(value) === '') return;
const id = uuid();
@ -217,12 +220,12 @@ export function useSendMultipleChatMessage(
role: MessageType.User,
files,
conversationId: targetConversationId,
reasoning: enableThinking,
internet: enableInternet,
},
chatBoxId,
currentConversationId: targetConversationId,
messages: currentMessages,
enableThinking,
enableInternet,
});
}
});

View File

@ -66,14 +66,19 @@ export const useSendSharedMessage = () => {
const [hasError, setHasError] = useState(false);
const sendMessage = useCallback(
async (message: Message, id?: string) => {
async (
message: Message,
id?: string,
enableThinking?: boolean,
enableInternet?: boolean,
) => {
const res = await send({
conversation_id: id ?? conversationId,
quote: true,
question: message.content,
session_id: get(derivedMessages, '0.session_id'),
reasoning: message.reasoning,
internet: message.internet,
reasoning: enableThinking,
internet: enableInternet,
});
if (isCompletionError(res)) {
@ -86,14 +91,18 @@ export const useSendSharedMessage = () => {
);
const handleSendMessage = useCallback(
async (message: Message) => {
async (
message: Message,
enableThinking?: boolean,
enableInternet?: boolean,
) => {
if (conversationId !== '') {
sendMessage(message);
sendMessage(message, undefined, enableThinking, enableInternet);
} else {
const data = await setConversation('user id');
if (data.code === 0) {
const id = data.data.id;
sendMessage(message, id);
sendMessage(message, id, enableThinking, enableInternet);
}
}
},
@ -120,11 +129,10 @@ export const useSendSharedMessage = () => {
}, [answer, addNewestAnswer]);
const handlePressEnter = useCallback(
(
...[
{ enableThinking, enableInternet },
]: NextMessageInputOnPressEnterParameter
) => {
({
enableThinking,
enableInternet,
}: NextMessageInputOnPressEnterParameter) => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
@ -135,13 +143,15 @@ export const useSendSharedMessage = () => {
id,
role: MessageType.User,
});
handleSendMessage({
content: value.trim(),
id,
role: MessageType.User,
reasoning: enableThinking,
internet: enableInternet,
});
handleSendMessage(
{
content: value.trim(),
id,
role: MessageType.User,
},
enableThinking,
enableInternet,
);
}
},
[addNewestQuestion, done, handleSendMessage, setValue, value],