refactor input box with no noticable errors

This commit is contained in:
Mathew Pareles 2025-02-04 01:20:33 -08:00
parent 5c4753555e
commit cd983f0dd9
4 changed files with 188 additions and 181 deletions

View file

@ -29,6 +29,7 @@
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"cookie": "^0.4.0",
"debounced": "1.0.2",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
@ -396,6 +397,12 @@
"node": ">= 0.6"
}
},
"node_modules/debounced": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz",
"integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View file

@ -7,7 +7,7 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'reac
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
import { ButtonStop, ButtonSubmit, IconX, VoidInputForm } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
@ -49,12 +49,11 @@ export const QuickEditChat = ({
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
const onSubmit = useCallback((e: FormEvent) => {
const onSubmit = useCallback(() => {
if (isDisabled) return
if (currStreamingDiffZoneRef.current !== null) return
textAreaFnsRef.current?.disable()
const instructions = textAreaRef.current?.value ?? ''
const id = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
@ -80,109 +79,41 @@ export const QuickEditChat = ({
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
<VoidInputForm
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
isStreaming={isStreaming}
isDisabled={isDisabled}
featureName="Ctrl+K"
className="py-2 w-full"
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative`}
>
<div className='flex flex-row items-center justify-between items-end gap-1'>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full`}
>
{/* text input */}
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
// if presses the esc key, X
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder={`Enter instructions...`}
// ${keybindingString} to select.
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
multiline={true}
/>
</div>
{/* X button */}
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={12}
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onX}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder="Enter instructions..."
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
return
}
</div>
</div>
</form>
}}
multiline={true}
/>
</VoidInputForm>
</div>

View file

@ -6,6 +6,7 @@
import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { useSettingsState } from '../util/services.js';
export const ErrorDisplay = ({
@ -23,8 +24,7 @@ export const ErrorDisplay = ({
const details = errorDetails(fullError)
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + ''
const message = message_ === 'TypeError: fetch failed' ? `TypeError for : fetch failed. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.` : message_ + ''
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>

View file

@ -3,10 +3,10 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
import { BlockCode } from '../markdown/BlockCode.js';
@ -22,6 +22,7 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { Pencil } from 'lucide-react';
import { FeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
@ -133,6 +134,103 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
}
interface VoidInputFormProps {
// Required
children: React.ReactNode; // This will be the input component
// Form controls
onSubmit: () => void;
onAbort: () => void;
isStreaming: boolean;
isDisabled?: boolean;
formRef?: React.RefObject<HTMLFormElement>;
// UI customization
featureName: FeatureName;
className?: string;
showModelDropdown?: boolean;
showSelections?: boolean;
selections?: any[];
onSelectionsChange?: (selections: any[]) => void;
// Optional close button
onClose?: () => void;
}
export const VoidInputForm: React.FC<VoidInputFormProps> = ({
children,
onSubmit,
onAbort,
onClose,
formRef,
isStreaming = false,
isDisabled = false,
className = '',
showModelDropdown = true,
featureName,
showSelections = false,
selections = [],
onSelectionsChange,
}) => {
return (
<form
ref={formRef}
className={`
flex flex-col gap-1 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
${className}
`}
>
{/* Selections section */}
{showSelections && onSelectionsChange && (
<SelectedFiles
type='staging'
selections={selections}
setSelections={onSelectionsChange}
/>
)}
{/* Input section */}
<div className="relative w-full">
{children}
{/* Close button (X) if onClose is provided */}
{onClose && (
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={12}
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onClose}
/>
</div>
)}
</div>
{/* Bottom row */}
<div className='flex flex-row justify-between items-end gap-1'>
{showModelDropdown && (
<div className='max-w-[150px] @@[&_select]:!void-border-none @@[&_select]:!void-outline-none flex-grow'>
<ModelDropdown featureName={featureName} />
</div>
)}
{isStreaming ? (
<ButtonStop onClick={onAbort} />
) : (
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
)}
</div>
</form>
);
};
const useResizeObserver = () => {
const ref = useRef(null);
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
@ -565,20 +663,31 @@ export const SidebarChat = () => {
useScrollbarStyles(sidebarRef)
const onSubmit = async () => {
const onSubmit = useCallback(async () => {
console.log('onSubmit')
if (isDisabled) return
if (isStreaming) return
console.log('chatThreadsService', chatThreadsService ? chatThreadsService : '!undefined')
// send message to LLM
const userMessage = textAreaRef.current?.value ?? ''
console.log('userMessage', userMessage)
console.log('streaming...',)
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
console.log('done streaming',)
chatThreadsService.setStaging([]) // clear staging
console.log('set staging',)
textAreaFnsRef.current?.setValue('')
console.log('set value',)
textAreaRef.current?.focus() // focus input after submit
console.log('textAreaRef', textAreaRef.current)
console.log('focus',)
}
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef])
const onAbort = () => {
const threadId = currentThread.id
@ -611,6 +720,7 @@ export const SidebarChat = () => {
</div>
const messagesHTML = <ScrollToBottomContainer
scrollContainerRef={scrollContainerRef}
className={`
@ -646,77 +756,36 @@ export const SidebarChat = () => {
</ScrollToBottomContainer>
const inputBox = <div // this div is used to position the input box properly
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<div
ref={formRef}
className={`
flex flex-col gap-1 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
max-h-[80vh] overflow-y-auto
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
const onChangeText = useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
}, [setInstructionsAreEmpty])
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}, [onSubmit])
const inputForm = <div className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
<VoidInputForm
formRef={formRef}
onSubmit={onSubmit}
onAbort={onAbort}
isStreaming={isStreaming}
isDisabled={isDisabled}
showSelections={true}
selections={selections || []}
onSelectionsChange={chatThreadsService.setStaging.bind(chatThreadsService)}
featureName="Ctrl+L"
>
{/* top row */}
<>
{/* selections */}
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0} />
</>
{/* middle row */}
<div>
{/* text input */}
<VoidInputBox2
className='min-h-[81px] p-1'
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none
flex-grow
'
>
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onAbort}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
}
</div>
</div>
<VoidInputBox2
className='min-h-[81px] p-1'
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
onChangeText={onChangeText}
onKeyDown={onKeyDown}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidInputForm>
</div>
return <div ref={sidebarRef} className={`w-full h-full`}>
@ -724,7 +793,7 @@ export const SidebarChat = () => {
{messagesHTML}
{inputBox}
{inputForm}
</div>
}