mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
sidebar + ux
This commit is contained in:
parent
0589a2a5ec
commit
a42ff2d59a
5 changed files with 129 additions and 55 deletions
|
|
@ -24,8 +24,8 @@ export const Sidebar = () => {
|
|||
const { isHistoryOpen, currentTab: tab } = sidebarState
|
||||
|
||||
// className='@@void-scope'
|
||||
return <div className='@@void-scope'>
|
||||
<div className={`flex flex-col w-full px-2 py-2`}>
|
||||
return <div className='@@void-scope w-full h-full'>
|
||||
<div className={`flex flex-col px-2 py-2 w-full h-full`}>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
|
|
@ -33,18 +33,28 @@ export const Sidebar = () => {
|
|||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`mb-2 ${isHistoryOpen ? '' : 'hidden'}`}>
|
||||
<div className={`mb-2 w-full h-full ${isHistoryOpen ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarChat />
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* <ErrorBoundary>
|
||||
<ModelSelectionSettings />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
|
||||
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings />
|
||||
</ErrorBoundary>
|
||||
</div> */}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,52 @@ const IconSquare = ({ size, className = '' }: { size: number, className?: string
|
|||
};
|
||||
|
||||
|
||||
const ScrollToBottomContainer = ({ children, height, className }: { children: React.ReactNode, height: React.CSSProperties['height'], className?: string }) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (divRef.current) {
|
||||
divRef.current.scrollTop = divRef.current.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = () => {
|
||||
const div = divRef.current;
|
||||
if (!div) return;
|
||||
|
||||
const isBottom = Math.abs(
|
||||
div.scrollHeight - div.clientHeight - div.scrollTop
|
||||
) < 1;
|
||||
|
||||
setIsAtBottom(isBottom);
|
||||
};
|
||||
|
||||
// When children change (new messages added)
|
||||
useEffect(() => {
|
||||
if (isAtBottom) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [children, isAtBottom]); // Dependency on children to detect new messages
|
||||
|
||||
// Initial scroll to bottom
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
onScroll={onScroll}
|
||||
style={{ height: height }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// read files from VSCode
|
||||
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
|
||||
const model = modelService.getModel(uri)
|
||||
|
|
@ -112,7 +158,7 @@ export const SelectedFiles = (
|
|||
|
||||
return (
|
||||
!!selections && selections.length !== 0 && (
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
<div className='flex flex-wrap gap-4 p-2'>
|
||||
{selections.map((selection, i) => (
|
||||
<Fragment key={i}>
|
||||
{/* selected file summary */}
|
||||
|
|
@ -122,6 +168,7 @@ export const SelectedFiles = (
|
|||
select-none
|
||||
bg-vscode-badge-bg border border-vscode-button-border rounded-md
|
||||
w-fit h-fit min-w-[80px] p-1
|
||||
text-left
|
||||
`}
|
||||
onClick={() => {
|
||||
setSelectionIsOpened(s => {
|
||||
|
|
@ -145,9 +192,11 @@ export const SelectedFiles = (
|
|||
{/* X button */}
|
||||
{type === 'staging' && // hoveredIdx === i
|
||||
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
|
||||
onClick={() => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
|
|
@ -252,7 +301,6 @@ export const SidebarChat = () => {
|
|||
const isDisabled = !instructions.trim()
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
|
||||
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
|
|
@ -359,7 +407,10 @@ export const SidebarChat = () => {
|
|||
const previousMessages = currentThread?.messages ?? []
|
||||
|
||||
return <>
|
||||
<div className="overflow-x-hidden space-y-4">
|
||||
<ScrollToBottomContainer
|
||||
height={`calc(100%-${formRef.current?.height ?? 0}px)`}
|
||||
className='overflow-x-hidden overflow-y-auto space-y-4'
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
|
|
@ -367,23 +418,22 @@ export const SidebarChat = () => {
|
|||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} />
|
||||
</div>
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
|
||||
{/* input box */}
|
||||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2
|
||||
${previousMessages.length === 0 ? '' : 'absolute bottom-0'}
|
||||
`}
|
||||
className={`right-0 left-0 m-2 z-[999] ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<form
|
||||
ref={formRef}
|
||||
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-vscode-commandcenter-border hover:border-vscode-commandcenter-active-border
|
||||
`}
|
||||
className={`
|
||||
flex flex-col gap-2 px-2 py-0.5 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
|
|
@ -434,19 +484,21 @@ export const SidebarChat = () => {
|
|||
{isLoading ?
|
||||
// stop button
|
||||
<button
|
||||
className="p-[5px] bg-white rounded-full cursor-pointer"
|
||||
className={`size-[24px] rounded-full bg-white cursor-pointer`}
|
||||
onClick={onAbort}
|
||||
type='button'
|
||||
>
|
||||
<IconSquare size={24} className="stroke-[2]" />
|
||||
<IconSquare size={16} className="stroke-[2]" />
|
||||
</button>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<button
|
||||
className={`${isDisabled ? 'bg-vscode-disabled-fg cursor-not-allowed' : 'bg-white cursor-pointer'}
|
||||
rounded-full
|
||||
shrink-0 grow-0
|
||||
`}
|
||||
className={`size-[24px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
${isDisabled ?
|
||||
'bg-vscode-disabled-fg' // cursor-not-allowed
|
||||
: 'bg-white' // cursor-pointer
|
||||
}
|
||||
`}
|
||||
disabled={isDisabled}
|
||||
type='submit'
|
||||
>
|
||||
|
|
|
|||
|
|
@ -38,31 +38,31 @@ module.exports = {
|
|||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-placeholder-fg": "input-var(--vscode-placeholderForeground)",
|
||||
"input-active-bg": "inputOption-var(--vscode-activeBackground)",
|
||||
"input-option-active-border": "inputOption-var(--vscode-activeBorder)",
|
||||
"input-option-active-fg": "inputOption-var(--vscode-activeForeground)",
|
||||
"input-option-hover-bg": "inputOption-var(--vscode-hoverBackground)",
|
||||
"input-validation-error-bg": "inputValidation-var(--vscode-errorBackground)",
|
||||
"input-validation-error-fg": "inputValidation-var(--vscode-errorForeground)",
|
||||
"input-validation-error-border": "inputValidation-var(--vscode-errorBorder)",
|
||||
"input-validation-info-bg": "inputValidation-var(--vscode-infoBackground)",
|
||||
"input-validation-info-fg": "inputValidation-var(--vscode-infoForeground)",
|
||||
"input-validation-info-border": "inputValidation-var(--vscode-infoBorder)",
|
||||
"input-validation-warning-bg": "inputValidation-var(--vscode-warningBackground)",
|
||||
"input-validation-warning-fg": "inputValidation-var(--vscode-warningForeground)",
|
||||
"input-validation-warning-border": "inputValidation-var(--vscode-warningBorder)",
|
||||
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-activeBackground)",
|
||||
"input-option-active-border": "var(--vscode-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-hoverBackground)",
|
||||
"input-validation-error-bg": "var(--vscode-errorBackground)",
|
||||
"input-validation-error-fg": "var(--vscode-errorForeground)",
|
||||
"input-validation-error-border": "var(--vscode-errorBorder)",
|
||||
"input-validation-info-bg": "var(--vscode-infoBackground)",
|
||||
"input-validation-info-fg": "var(--vscode-infoForeground)",
|
||||
"input-validation-info-border": "var(--vscode-infoBorder)",
|
||||
"input-validation-warning-bg": "var(--vscode-warningBackground)",
|
||||
"input-validation-warning-fg": "var(--vscode-warningForeground)",
|
||||
"input-validation-warning-border": "var(--vscode-warningBorder)",
|
||||
|
||||
// command center colors (the top bar)
|
||||
"commandcenter-fg": "commandCenter.foreground",
|
||||
"commandcenter-active-fg": "commandCenter.activeForeground",
|
||||
"commandcenter-bg": "commandCenter.background",
|
||||
"commandcenter-active-bg": "commandCenter.activeBackground",
|
||||
"commandcenter-border": "commandCenter.border",
|
||||
"commandcenter-inactive-fg": "commandCenter.inactiveForeground",
|
||||
"commandcenter-inactive-border": "commandCenter.inactiveBorder",
|
||||
"commandcenter-active-border": "commandCenter.activeBorder",
|
||||
"commandcenter-debugging-bg": "commandCenter.debuggingBackground",
|
||||
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
|
||||
"commandcenter-active-fg": "var(--vscode-commandCenter-activeForeground)",
|
||||
"commandcenter-bg": "var(--vscode-commandCenter-background)",
|
||||
"commandcenter-active-bg": "var(--vscode-commandCenter-activeBackground)",
|
||||
"commandcenter-border": "var(--vscode-commandCenter-border)",
|
||||
"commandcenter-inactive-fg": "var(--vscode-commandCenter-inactiveForeground)",
|
||||
"commandcenter-inactive-border": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"commandcenter-active-border": "var(--vscode-commandCenter-activeBorder)",
|
||||
"commandcenter-debugging-bg": "var(--vscode-commandCenter-debuggingBackground)",
|
||||
|
||||
// badge colors
|
||||
"badge-fg": "var(--vscode-badge-foreground)",
|
||||
|
|
@ -84,7 +84,6 @@ module.exports = {
|
|||
"checkbox-border": "var(--vscode-checkbox-border)",
|
||||
"checkbox-select-bg": "var(--vscode-checkbox-selectBackground)",
|
||||
|
||||
|
||||
// sidebar colors
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"sidebar-fg": "var(--vscode-sideBar-foreground)",
|
||||
|
|
@ -101,7 +100,6 @@ module.exports = {
|
|||
"sidebar-stickyscroll-border": "var(--vscode-sideBarStickyScroll-border)",
|
||||
"sidebar-stickyscroll-shadow": "var(--vscode-sideBarStickyScroll-shadow)",
|
||||
|
||||
|
||||
// other colors (these are partially complete)
|
||||
|
||||
// editor colors
|
||||
|
|
@ -113,7 +111,6 @@ module.exports = {
|
|||
"editorwidget-bg": "var(--vscode-editorWidget-background)",
|
||||
"editorwidget-border": "var(--vscode-editorWidget-border)",
|
||||
|
||||
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -83,9 +83,18 @@ registerAction2(class extends Action2 {
|
|||
)
|
||||
|
||||
if (selectionRange) {
|
||||
const selection: CodeStagingSelection = {
|
||||
selectionStr: getContentInRange(model, selectionRange),
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = selectionStr === null ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,15 @@ export type CodeSelection = {
|
|||
|
||||
// if selectionStr is null, it means to use the entire file at send time
|
||||
export type CodeStagingSelection = {
|
||||
fileURI: URI;
|
||||
selectionStr: string | null;
|
||||
range: IRange;
|
||||
type: 'Selection',
|
||||
fileURI: URI,
|
||||
selectionStr: string,
|
||||
range: IRange
|
||||
} | {
|
||||
type: 'File',
|
||||
fileURI: URI,
|
||||
selectionStr: null,
|
||||
range: null
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue