Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor improvements: main editor max height, collapsible codeblocks #3898

Merged
merged 6 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gui/src/components/mainInput/CodeBlockComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CodeSnippetPreview from "../markdown/CodeSnippetPreview";
export const CodeBlockComponent = (props: any) => {
const { node, deleteNode, selected, editor, updateAttributes } = props;
const item: ContextItemWithId = node.attrs.item;
const inputId = node.attrs.inputId;
// const contextItems = useSelector(
// (store: RootState) =>
// store.session.messages[store.session.messages.length - 1].contextItems,
Expand All @@ -23,6 +24,7 @@ export const CodeBlockComponent = (props: any) => {
as={nodeViewWrapperTag}
>
<CodeSnippetPreview
inputId={inputId}
borderColor={
isFirstContextItem
? "#d0d"
Expand Down
3 changes: 3 additions & 0 deletions gui/src/components/mainInput/CodeBlockExtension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const CodeBlockExtension = Node.create({
item: {
default: "",
},
inputId: {
default: "",
},
};
},

Expand Down
2 changes: 2 additions & 0 deletions gui/src/components/mainInput/ContinueInputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface ContinueInputBoxProps {
editorState?: JSONContent;
contextItems?: ContextItemWithId[];
hidden?: boolean;
inputId: string; // used to keep track of things per input in redux
}

const EDIT_DISALLOWED_CONTEXT_PROVIDERS = [
Expand Down Expand Up @@ -132,6 +133,7 @@ function ContinueInputBox(props: ContinueInputBoxProps) {
availableSlashCommands={filteredSlashCommands}
historyKey={historyKey}
toolbarOptions={toolbarOptions}
inputId={props.inputId}
/>
</GradientBorder>
</div>
Expand Down
36 changes: 12 additions & 24 deletions gui/src/components/mainInput/TipTapEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
selectHasCodeToEdit,
selectIsInEditMode,
setMainEditorContentTrigger,
setNewestCodeblocksForInput,
} from "../../redux/slices/sessionSlice";
import { exitEditMode } from "../../redux/thunks";
import {
Expand Down Expand Up @@ -108,11 +109,6 @@ const InputBoxDiv = styled.div<{ border?: string }>`
flex-direction: column;
`;

const PaddingDiv = styled.div`
padding: 8px 12px;
padding-bottom: 4px;
`;

const HoverDiv = styled.div`
position: absolute;
width: 100%;
Expand Down Expand Up @@ -180,6 +176,7 @@ interface TipTapEditorProps {
border?: string;
placeholder?: string;
historyKey: string;
inputId: string;
}

export const TIPPY_DIV_ID = "tippy-js-div";
Expand Down Expand Up @@ -782,22 +779,6 @@ function TipTapEditor(props: TipTapEditorProps) {
return;
}

// const rif: RangeInFile & { contents: string } =
// data.rangeInFileWithContents;
// const basename = getBasename(rif.filepath);
// const relativePath = getRelativePath(
// rif.filepath,
// await ideMessenger.ide.getWorkspaceDirs(),
// const rangeStr = `(${rif.range.start.line + 1}-${
// rif.range.end.line + 1
// })`;

// const itemName = `${basename} ${rangeStr}`;
// const item: ContextItemWithId = {
// content: rif.contents,
// name: itemName
// }

const contextItem = rifWithContentsToContextItem(
data.rangeInFileWithContents,
);
Expand All @@ -819,10 +800,16 @@ function TipTapEditor(props: TipTapEditorProps) {
type: "codeBlock",
attrs: {
item: contextItem,
inputId: props.inputId,
},
})
.run();

dispatch(
setNewestCodeblocksForInput({
inputId: props.inputId,
contextItemId: contextItem.id.itemId,
}),
);
if (data.prompt) {
editor.commands.focus("end");
editor.commands.insertContent(data.prompt);
Expand Down Expand Up @@ -990,8 +977,9 @@ function TipTapEditor(props: TipTapEditorProps) {
event.preventDefault();
}}
>
<PaddingDiv>
<div className="px-2.5 pb-1 pt-2">
<EditorContent
className={`scroll-container overflow-y-scroll ${props.isMainInput ? "max-h-[70vh]" : ""}`}
spellCheck={false}
editor={editor}
onClick={(event) => {
Expand Down Expand Up @@ -1023,7 +1011,7 @@ function TipTapEditor(props: TipTapEditorProps) {
}}
disabled={isStreaming}
/>
</PaddingDiv>
</div>

{showDragOverMsg &&
modelSupportsImages(
Expand Down
136 changes: 81 additions & 55 deletions gui/src/components/markdown/CodeSnippetPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
ChevronDownIcon,
ChevronUpIcon,
EyeSlashIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import { ContextItemWithId } from "core";
import { dedent, getMarkdownLanguageTagForFile } from "core/util";
import React, { useContext, useMemo } from "react";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components";
import { defaultBorderRadius, lightGray, vscEditorBackground } from "..";
import { IdeMessengerContext } from "../../context/IdeMessenger";
Expand All @@ -14,6 +14,8 @@ import FileIcon from "../FileIcon";
import HeaderButtonWithToolTip from "../gui/HeaderButtonWithToolTip";
import StyledMarkdownPreview from "./StyledMarkdownPreview";
import { ctxItemToRifWithContents } from "core/commands/util";
import { EyeIcon } from "@heroicons/react/24/solid";
import { useAppSelector } from "../../redux/hooks";

const PreviewMarkdownDiv = styled.div<{
borderColor?: string;
Expand All @@ -31,32 +33,30 @@ const PreviewMarkdownDiv = styled.div<{
}
`;

const PreviewMarkdownHeader = styled.div`
margin: 0;
padding: 2px 6px;
border-bottom: 0.5px solid ${lightGray};
word-break: break-all;
font-size: ${getFontSize() - 3}px;
display: flex;
align-items: center;
`;

interface CodeSnippetPreviewProps {
item: ContextItemWithId;
onDelete?: () => void;
borderColor?: string;
hideHeader?: boolean;
inputId: string;
}

const MAX_PREVIEW_HEIGHT = 300;
const MAX_PREVIEW_HEIGHT = 100;

const backticksRegex = /`{3,}/gm;

function CodeSnippetPreview(props: CodeSnippetPreviewProps) {
const ideMessenger = useContext(IdeMessengerContext);

const [collapsed, setCollapsed] = React.useState(true);
const [hovered, setHovered] = React.useState(false);
const [localHidden, setLocalHidden] = useState<boolean | undefined>();
const [isSizeLimited, setIsSizeLimited] = useState(true);

const newestCodeblockForInputId = useAppSelector(
(store) => store.session.newestCodeblockForInput[props.inputId],
);

const hidden = useMemo(() => {
return localHidden ?? newestCodeblockForInputId !== props.item.id.itemId;
}, [localHidden, newestCodeblockForInputId, props.item]);

const content = useMemo(() => {
return dedent`${props.item.content}`;
Expand All @@ -67,20 +67,45 @@ function CodeSnippetPreview(props: CodeSnippetPreviewProps) {
return backticks ? backticks.sort().at(-1) + "`" : "```";
}, [content]);

const codeBlockRef = React.useRef<HTMLDivElement>(null);
const codeBlockRef = useRef<HTMLDivElement>(null);

const [codeblockDims, setCodeblockDims] = useState({ width: 0, height: 0 });
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
setCodeblockDims({
width: codeBlockRef.current?.scrollWidth ?? 0,
height: codeBlockRef.current?.scrollHeight ?? 0,
});
});

if (codeBlockRef.current) {
resizeObserver.observe(codeBlockRef.current);
}

return () => {
resizeObserver.disconnect();
};
}, [codeBlockRef]);

return (
<PreviewMarkdownDiv
spellCheck={false}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
borderColor={props.borderColor}
className="find-widget-skip"
>
{!props.hideHeader && (
<PreviewMarkdownHeader
className="flex cursor-pointer justify-between"
onClick={() => {
<div
className="m-0 flex cursor-pointer items-center justify-between break-all border-b px-[5px] py-1.5 hover:opacity-90"
style={{
fontSize: getFontSize() - 3,
}}
onClick={() => {
setLocalHidden(!hidden);
}}
>
<div
className="flex items-center gap-1 hover:underline"
onClick={(e) => {
e.stopPropagation();
if (
props.item.id.providerTitle === "file" &&
props.item.uri?.value
Expand All @@ -103,52 +128,53 @@ function CodeSnippetPreview(props: CodeSnippetPreviewProps) {
}
}}
>
<div className="flex items-center gap-1">
<FileIcon height="16px" width="16px" filename={props.item.name} />
{props.item.name}
</div>
<div className="flex items-center gap-1">
<HeaderButtonWithToolTip
text="Delete"
onClick={(e) => {
e.stopPropagation();
props.onDelete?.();
}}
>
<XMarkIcon width="1em" height="1em" />
</HeaderButtonWithToolTip>
</div>
</PreviewMarkdownHeader>
)}
<FileIcon height="16px" width="16px" filename={props.item.name} />
{props.item.name}
</div>
<div className="flex items-center gap-1">
<HeaderButtonWithToolTip text={hidden ? "Show" : "Hide"}>
{hidden ? (
<EyeIcon width="1em" height="1em" />
) : (
<EyeSlashIcon width="1em" height="1em" />
)}
</HeaderButtonWithToolTip>
<HeaderButtonWithToolTip
text="Delete"
onClick={(e) => {
e.stopPropagation();
props.onDelete?.();
}}
>
<XMarkIcon width="1em" height="1em" />
</HeaderButtonWithToolTip>
</div>
</div>
<div
contentEditable={false}
className={`m-0 ${collapsed ? "overflow-hidden" : "overflow-auto"}`}
className={`m-0 ${isSizeLimited ? "overflow-hidden" : "overflow-auto"} ${hidden ? "hidden" : ""}`}
ref={codeBlockRef}
style={{
maxHeight: collapsed ? MAX_PREVIEW_HEIGHT : undefined, // Could switch to max-h-[33vh] but then chevron icon shows when height can't change
maxHeight: isSizeLimited ? MAX_PREVIEW_HEIGHT : undefined, // Could switch to max-h-[33vh] but then chevron icon shows when height can't change
}}
>
<StyledMarkdownPreview
source={`${fence}${getMarkdownLanguageTagForFile(props.item.name)} ${props.item.description}\n${content}\n${fence}`}
/>
</div>

{(codeBlockRef.current?.scrollHeight ?? 0) > MAX_PREVIEW_HEIGHT && (
{codeblockDims.height > MAX_PREVIEW_HEIGHT && (
<HeaderButtonWithToolTip
className="absolute bottom-1 right-2"
text={collapsed ? "Expand" : "Collapse"}
text={isSizeLimited ? "Expand" : "Collapse"}
>
{collapsed ? (
<ChevronDownIcon
className="h-5 w-5"
onClick={() => setCollapsed(false)}
/>
) : (
<ChevronUpIcon
className="h-5 w-5"
onClick={() => setCollapsed(true)}
/>
)}
<ChevronDownIcon
className="h-5 w-5 transition-all"
style={{
transform: isSizeLimited ? "" : "rotate(180deg)",
}}
onClick={() => setIsSizeLimited((v) => !v)}
/>
</HeaderButtonWithToolTip>
)}
</PreviewMarkdownDiv>
Expand Down
1 change: 1 addition & 0 deletions gui/src/editorInset/EditorInset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function EditorInset() {
console.log("Enter: ", e, modifiers);
}}
historyKey="chat"
inputId="editor-inset"
/>
</EditorInsetDiv>
);
Expand Down
29 changes: 29 additions & 0 deletions gui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,32 @@ body {
direction: rtl;
width: 100%;
}

.scroll-container {
overflow-y: auto; /* Enable vertical scrolling only if needed */
scrollbar-width: thin; /* Firefox */
padding-right: 4px;
}

.scroll-container::-webkit-scrollbar {
width: 8px;
}

.scroll-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}

.scroll-container::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 4px;
transition: background-color 0.3s ease;
}

.scroll-container::-webkit-scrollbar-thumb:hover {
background-color: #555;
}

.scroll-container:is(:hover, :focus)::-webkit-scrollbar {
display: block; /* Show scrollbar only on hover or focus */
}
2 changes: 2 additions & 0 deletions gui/src/pages/gui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export function Chat() {
isMainInput={false}
editorState={item.editorState}
contextItems={item.contextItems}
inputId={item.message.id}
/>
</>
) : item.message.role === "tool" ? (
Expand Down Expand Up @@ -503,6 +504,7 @@ export function Chat() {
onEnter={(editorState, modifiers, editor) =>
sendInput(editorState, modifiers, undefined, editor)
}
inputId={"main-editor"}
/>
)}

Expand Down
Loading
Loading