mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
fix: smooth the handover into dictation
This commit is contained in:
parent
911959282a
commit
0e2dd60f14
3 changed files with 69 additions and 15 deletions
|
|
@ -243,11 +243,20 @@ function Bar({
|
|||
chipsFitInline(packedEntries, widths, rowWidth - plusWidth - controlsWidth - CHIP_GAP * 2),
|
||||
[packedEntries, widths, rowWidth, plusWidth, controlsWidth],
|
||||
);
|
||||
/* The recording controls take the bottom row over, so every chip moves into
|
||||
the block above and leaves with it rather than being cut off mid-row. */
|
||||
const dictating = dictation.active || dictation.transcribing;
|
||||
const above = inlineChips && !dictating ? EMPTY_ENTRIES : packedEntries;
|
||||
const inline = inlineChips && !dictating ? packedEntries : EMPTY_ENTRIES;
|
||||
|
||||
/* The arrangement is frozen for the length of a recording. The controls
|
||||
shrink to just the elapsed time while one runs, which would otherwise let
|
||||
chips qualify for the bottom row and re-mount there mid-animation: a chip
|
||||
that changes parent restarts from its new parent's resting state, which is
|
||||
no transition at all. */
|
||||
const [restingInline, setRestingInline] = useState(true);
|
||||
if (!dictating && restingInline !== inlineChips) {
|
||||
setRestingInline(inlineChips);
|
||||
}
|
||||
const chipsInline = dictating ? restingInline : inlineChips;
|
||||
const above = chipsInline ? EMPTY_ENTRIES : packedEntries;
|
||||
const inline = chipsInline ? packedEntries : EMPTY_ENTRIES;
|
||||
|
||||
const rowClass = cn(
|
||||
'flex flex-wrap items-center gap-1.5',
|
||||
|
|
@ -287,10 +296,16 @@ function Bar({
|
|||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{/* Travels down as the row above closes over it, so the chips read as
|
||||
moving out of the way rather than being wiped from the bottom up. */}
|
||||
<div
|
||||
role="list"
|
||||
aria-label={localize('com_ui_composer_tools')}
|
||||
className={cn(rowClass, 'pb-1.5')}
|
||||
className={cn(
|
||||
rowClass,
|
||||
'pb-1.5 transition-transform duration-200 ease-out',
|
||||
dictating ? 'translate-y-3' : 'translate-y-0',
|
||||
)}
|
||||
>
|
||||
{above.map(renderChip)}
|
||||
</div>
|
||||
|
|
@ -320,10 +335,19 @@ function Bar({
|
|||
anchorRef={anchorRef}
|
||||
/>
|
||||
</span>
|
||||
{/* `contents`, so these chips sit on the same line as the buttons rather
|
||||
than in a box of their own. The element keeps its list semantics for
|
||||
assistive tech; only its box is dropped. */}
|
||||
<div role="list" aria-label={localize('com_ui_composer_tools')} className="contents">
|
||||
{/* A box rather than `contents` so it has something to move: the split
|
||||
only puts chips here when they all fit on this line, so wrapping as
|
||||
one unit and wrapping as siblings come to the same layout. */}
|
||||
<div
|
||||
role="list"
|
||||
aria-label={localize('com_ui_composer_tools')}
|
||||
aria-hidden={dictating}
|
||||
className={cn(
|
||||
'flex min-w-0 flex-wrap items-center gap-1.5',
|
||||
'transition-[transform,opacity] duration-200 ease-out',
|
||||
dictating ? 'pointer-events-none translate-y-3 opacity-0' : 'translate-y-0 opacity-100',
|
||||
)}
|
||||
>
|
||||
{inline.map(renderChip)}
|
||||
</div>
|
||||
{/* Auto margin on the main-start side, so the group sits at the end of
|
||||
|
|
@ -354,14 +378,14 @@ function Bar({
|
|||
<Thinking />
|
||||
<TokenUsage index={index} conversation={conversation} isSubmitting={isSubmitting} />
|
||||
</div>
|
||||
{/* Fades in place. The elapsed time is not one of the things that
|
||||
stepped aside, so it has nowhere to travel from. */}
|
||||
<div
|
||||
aria-hidden={!dictating}
|
||||
className={cn(
|
||||
'col-start-1 row-start-1 flex items-center justify-end px-1',
|
||||
'transition-[transform,opacity] duration-200 ease-out',
|
||||
dictating
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'pointer-events-none translate-y-3 opacity-0',
|
||||
'transition-opacity duration-200 ease-out',
|
||||
dictating ? 'opacity-100' : 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<span className="text-xs tabular-nums text-text-secondary">
|
||||
|
|
|
|||
|
|
@ -662,7 +662,10 @@ function Palette({
|
|||
'hover:bg-surface-hover hover:text-text-primary',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
|
||||
'disabled:cursor-not-allowed disabled:opacity-40',
|
||||
open && 'bg-surface-hover text-text-primary',
|
||||
/* The close mark carries the same weight whichever job it is
|
||||
doing: dimmer and unbacked while dictating read as a smaller
|
||||
glyph next to the one the open palette shows. */
|
||||
(open || dictating) && 'bg-surface-hover text-text-primary',
|
||||
)}
|
||||
>
|
||||
{/* The same glyph turned a quarter of the way round is the close
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import useLocalize from '../useLocalize';
|
|||
|
||||
const isExternalSTT = (speechToTextEndpoint: string) => speechToTextEndpoint === 'external';
|
||||
|
||||
/** How long to hold the recording layout after a stop before giving up on a
|
||||
* transcription request ever being reported. */
|
||||
const SETTLE_MS = 500;
|
||||
|
||||
/** How the in-flight transcription should be spent once recording ends. */
|
||||
type StopMode = 'compose' | 'send' | 'cancel';
|
||||
|
||||
|
|
@ -123,6 +127,27 @@ export default function useDictation({
|
|||
const active = isListening === true;
|
||||
const levels = useAudioLevels(active);
|
||||
|
||||
/* Bridges the gap between the recorder being told to stop and the upload
|
||||
starting: `isListening` clears synchronously, while the recorder's own
|
||||
`stop` event, which is what begins the transcription request, arrives a
|
||||
tick later. Without this the composer reads as idle for that tick and every
|
||||
control it had put away flashes back in and out again. */
|
||||
const [settling, setSettling] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!settling) {
|
||||
return;
|
||||
}
|
||||
if (isLoading === true) {
|
||||
setSettling(false);
|
||||
return;
|
||||
}
|
||||
/* Backstop for the engines that never report a request at all: the browser
|
||||
recogniser hands its transcript straight over, so nothing else would ever
|
||||
clear this. */
|
||||
const timer = setTimeout(() => setSettling(false), SETTLE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [settling, isLoading]);
|
||||
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
|
|
@ -142,6 +167,7 @@ export default function useDictation({
|
|||
const stopWith = useCallback(
|
||||
(mode: StopMode) => {
|
||||
modeRef.current = mode;
|
||||
setSettling(true);
|
||||
stopRecording();
|
||||
},
|
||||
[stopRecording],
|
||||
|
|
@ -153,6 +179,7 @@ export default function useDictation({
|
|||
that was already in flight lands anyway. */
|
||||
const cancel = useCallback(() => {
|
||||
modeRef.current = 'cancel';
|
||||
setSettling(false);
|
||||
abortRecording();
|
||||
reset({ text: existingTextRef.current });
|
||||
existingTextRef.current = '';
|
||||
|
|
@ -160,7 +187,7 @@ export default function useDictation({
|
|||
|
||||
return {
|
||||
active,
|
||||
transcribing: isLoading === true,
|
||||
transcribing: isLoading === true || settling,
|
||||
levels,
|
||||
elapsed,
|
||||
start,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue