mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
refactor input box with no noticable errors
This commit is contained in:
parent
5c4753555e
commit
cd983f0dd9
4 changed files with 188 additions and 181 deletions
7
remote/package-lock.json
generated
7
remote/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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`}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue