sidebar + ux

This commit is contained in:
mp 2024-12-15 01:30:44 -08:00
parent 0589a2a5ec
commit a42ff2d59a
5 changed files with 129 additions and 55 deletions

View file

@ -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>

View file

@ -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'
>

View file

@ -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)",
},
},
},

View file

@ -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,
}

View file

@ -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
}