mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
feat: the composer is the free-form answer box (like the main chat input)
While a question pause is live, the main chat textarea composes the free-form answer — placeholder swaps to 'Something else…' (or a folded model 'Other' label), Enter with text submits the answer through answer-mode key handling (composed BEFORE useTextarea's submitting-lock, so the lock can't swallow it), and the Stop button swaps to Send (enabled despite isSubmitting) per the select-then-confirm design. The popover slims to the question header, numbered option rows, and Skip/Submit — its inline input is gone since the composer owns free-form now. Dismissing the popover restores normal composer semantics (Stop button, normal sends).
This commit is contained in:
parent
a92760ad05
commit
b215560d97
3 changed files with 49 additions and 55 deletions
|
|
@ -14,19 +14,8 @@ import { cn } from '~/utils';
|
|||
*/
|
||||
function AskUserQuestionPopoverContent({ conversationId }: { conversationId: string }) {
|
||||
const localize = useLocalize();
|
||||
const {
|
||||
liveAsk,
|
||||
active,
|
||||
options,
|
||||
selected,
|
||||
setSelected,
|
||||
otherText,
|
||||
setOtherText,
|
||||
canSubmit,
|
||||
submit,
|
||||
dismiss,
|
||||
otherLabel,
|
||||
} = useAskAnswerMode(conversationId);
|
||||
const { liveAsk, active, options, selected, setSelected, canSubmit, submit, dismiss } =
|
||||
useAskAnswerMode(conversationId);
|
||||
|
||||
if (!active || !liveAsk) {
|
||||
return null;
|
||||
|
|
@ -71,42 +60,6 @@ function AskUserQuestionPopoverContent({ conversationId }: { conversationId: str
|
|||
<span className="flex-1">{option.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-lg p-2 text-sm',
|
||||
selected === 'other' ? 'bg-surface-active' : 'hover:bg-surface-hover',
|
||||
)}
|
||||
>
|
||||
{options.length > 0 && (
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded bg-surface-tertiary text-xs text-text-secondary">
|
||||
{options.length + 1}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
value={otherText}
|
||||
onChange={(e) => {
|
||||
setOtherText(e.target.value);
|
||||
setSelected('other');
|
||||
}}
|
||||
onFocus={() => setSelected('other')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && canSubmit) {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
} else if (e.key === 'Escape') {
|
||||
dismiss();
|
||||
}
|
||||
}}
|
||||
placeholder={
|
||||
otherLabel ??
|
||||
(options.length > 0
|
||||
? localize('com_ui_something_else')
|
||||
: localize('com_ui_your_answer'))
|
||||
}
|
||||
className="flex-1 border-0 bg-transparent text-sm text-text-primary placeholder:text-text-secondary focus:outline-none"
|
||||
aria-label={localize('com_ui_your_answer')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 p-2">
|
||||
<Button size="sm" variant="outline" onClick={dismiss}>
|
||||
{localize('com_ui_skip')}
|
||||
|
|
|
|||
|
|
@ -191,7 +191,10 @@ const ChatForm = memo(function ChatForm({
|
|||
submitButtonRef,
|
||||
setIsScrollable,
|
||||
disabled: disableInputs,
|
||||
placeholder,
|
||||
// The composer IS the free-form answer box while a question pause is live.
|
||||
placeholder: answerMode.active
|
||||
? (answerMode.otherLabel ?? localize('com_ui_something_else'))
|
||||
: placeholder,
|
||||
});
|
||||
|
||||
useQueryParams({ textAreaRef });
|
||||
|
|
@ -248,7 +251,15 @@ const ChatForm = memo(function ChatForm({
|
|||
|
||||
return (
|
||||
<form
|
||||
onSubmit={methods.handleSubmit(submitMessage)}
|
||||
onSubmit={methods.handleSubmit((data) => {
|
||||
// Answer mode: composer text answers the paused run instead of
|
||||
// starting a new turn. Dismissing the popover restores normal sends.
|
||||
if (answerMode.active && answerMode.submitText(data.text)) {
|
||||
methods.reset();
|
||||
return;
|
||||
}
|
||||
return submitMessage(data);
|
||||
})}
|
||||
className={cn(
|
||||
'mx-auto flex w-full flex-row gap-3 transition-[max-width] duration-300 sm:px-2',
|
||||
maximizeChatSpace ? 'max-w-full' : 'md:max-w-3xl xl:max-w-4xl',
|
||||
|
|
@ -412,14 +423,19 @@ const ChatForm = memo(function ChatForm({
|
|||
/>
|
||||
)}
|
||||
<div className={`${isRTL ? 'ml-2' : 'mr-2'}`}>
|
||||
{isSubmitting && showStopButton ? (
|
||||
{isSubmitting && showStopButton && !answerMode.active ? (
|
||||
<StopButton stop={handleStopGenerating} setShowStopButton={setShowStopButton} />
|
||||
) : (
|
||||
endpoint && (
|
||||
<SendButton
|
||||
ref={submitButtonRef}
|
||||
control={methods.control}
|
||||
disabled={filesLoading || isSubmitting || disableInputs || isNotAppendable}
|
||||
disabled={
|
||||
filesLoading ||
|
||||
disableInputs ||
|
||||
isNotAppendable ||
|
||||
(isSubmitting && !answerMode.active)
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -88,13 +88,37 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
setOtherText,
|
||||
]);
|
||||
|
||||
/** Composer text answers the question directly; true when consumed. */
|
||||
const submitText = useCallback(
|
||||
(text: string): boolean => {
|
||||
if (!active || !liveAsk) {
|
||||
return false;
|
||||
}
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length > 0) {
|
||||
submitAskAnswer(liveAsk.actionId, trimmed);
|
||||
setSelected(null);
|
||||
setOtherText('');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[active, liveAsk, submitAskAnswer, setSelected, setOtherText],
|
||||
);
|
||||
|
||||
/** Selection steering from the empty composer; true when consumed. */
|
||||
const handleComposerKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>): boolean => {
|
||||
if (!active) {
|
||||
return false;
|
||||
}
|
||||
if (e.currentTarget.value.trim().length > 0) {
|
||||
const composerText = e.currentTarget.value;
|
||||
if (composerText.trim().length > 0) {
|
||||
// The composer IS the free-form answer box: Enter submits the typed
|
||||
// text (before useTextarea's submitting-lock can swallow it).
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
return submitText(composerText);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const rowCount = options.length + 1; // + the inline Other row
|
||||
|
|
@ -129,7 +153,7 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
[active, options, selected, canSubmit, submit, dismiss, setSelected],
|
||||
[active, options, selected, canSubmit, submit, submitText, dismiss, setSelected],
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -144,6 +168,7 @@ export default function useAskAnswerMode(conversationId?: string | null) {
|
|||
setOtherText,
|
||||
canSubmit,
|
||||
submit,
|
||||
submitText,
|
||||
handleComposerKeyDown,
|
||||
/** Model-supplied "Other"-style label, folded into the inline input. */
|
||||
otherLabel,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue