Skip to content

Commit

Permalink
style(code editor): improved UI / UX for code editor (All-Hands-AI#826)
Browse files Browse the repository at this point in the history
* style(): improved code edito ui / ux

* fix(): fix build issue and use cn fn

* theme variable updated to tailwind neutral gray

* fix(): fix conflicts

* fix lint errors

---------

Co-authored-by: Jim Su <[email protected]>
  • Loading branch information
akhilvc10 and yimothysu authored Apr 8, 2024
1 parent 6b7c5b0 commit 6f795f5
Show file tree
Hide file tree
Showing 15 changed files with 429 additions and 299 deletions.
178 changes: 82 additions & 96 deletions frontend/package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@
"@reduxjs/toolkit": "^2.2.2",
"@vitejs/plugin-react": "^4.2.1",
"@xterm/xterm": "^5.4.0",
"clsx": "^2.1.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^11.0.24",
"jose": "^5.2.3",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.1",
"i18next-http-backend": "^2.5.0",
"jose": "^5.2.3",
"monaco-editor": "^0.47.0",
"react": "^18.2.0",
"react-accessible-treeview": "^2.8.3",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-i18next": "^14.1.0",
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-syntax-highlighter": "^15.5.0",
"tailwind-merge": "^2.2.2",
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface Props {

function LeftNav({ setSettingOpen }: Props): JSX.Element {
return (
<div className="flex flex-col h-full p-4 bg-bg-dark w-16 items-center shrink-0">
<div className="flex flex-col h-full p-4 bg-neutral-900 w-16 items-center shrink-0">
<div
className="mt-auto cursor-pointer hover:opacity-80"
onClick={() => setSettingOpen(true)}
Expand Down Expand Up @@ -69,18 +69,18 @@ function App(): JSX.Element {
};

return (
<div className="flex h-screen bg-bg-dark text-white">
<div className="flex h-screen bg-neutral-900 text-white">
<LeftNav setSettingOpen={setSettingOpen} />
<div className="flex flex-col grow gap-3 py-3 pr-3">
<div className="flex gap-3 grow min-h-0">
<div className="w-[500px] shrink-0 rounded-xl overflow-hidden border border-border">
<div className="w-[500px] shrink-0 rounded-xl overflow-hidden border border-neutral-600">
<ChatInterface />
</div>
<div className="flex flex-col flex-1 overflow-hidden rounded-xl bg-bg-workspace border border-border">
<div className="flex flex-col flex-1 overflow-hidden rounded-xl bg-neutral-800 border border-neutral-600">
<Workspace />
</div>
</div>
<div className="h-72 shrink-0 bg-bg-workspace rounded-xl border border-border flex flex-col">
<div className="h-72 shrink-0 bg-neutral-800 rounded-xl border border-neutral-600 flex flex-col">
<Terminal key="terminal" />
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Browser(): JSX.Element {
: `data:image/png;base64,${screenshotSrc || ""}`;

return (
<div className="h-full m-2 bg-bg-workspace mockup-browser">
<div className="h-full m-2 bg-neutral-700 mockup-browser">
<div className="mockup-browser-toolbar">
<div className="input">{url}</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ function ChatInterface(): JSX.Element {
const { initialized } = useSelector((state: RootState) => state.task);

return (
<div className="flex flex-col h-full p-0 bg-bg-workspace">
<div className="border-b border-border text-lg px-4 py-2">Chat</div>
<div className="flex flex-col h-full p-0 bg-neutral-800">
<div className="border-b border-neutral-600 text-sm px-4 py-2">Chat</div>
{initialized ? <MessageList /> : <InitializingStatus />}
<Input />
</div>
Expand Down
219 changes: 52 additions & 167 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,166 +1,17 @@
import Editor, { Monaco } from "@monaco-editor/react";
import type { editor } from "monaco-editor";
import React, { useEffect, useState } from "react";
import TreeView, { flattenTree } from "react-accessible-treeview";
import { DiJavascript } from "react-icons/di";
import {
FaCss3,
FaFile,
FaFolder,
FaFolderOpen,
FaHtml5,
FaList,
FaMarkdown,
FaNpm,
FaPython,
} from "react-icons/fa";
import { VscClose, VscListTree, VscRefresh } from "react-icons/vsc";
import React, { useState } from "react";
import { Tabs, Tab } from "@nextui-org/react";
import { useSelector } from "react-redux";
import { getWorkspace, selectFile } from "../services/fileService";
import { setCode, updateWorkspace } from "../state/codeSlice";
import store, { RootState } from "../store";

interface FileIconProps {
filename: string;
}

function FileIcon({ filename }: FileIconProps): JSX.Element | null {
const extension = filename.slice(filename.lastIndexOf(".") + 1);
switch (extension) {
case "js":
return <DiJavascript />;
case "ts":
return <DiJavascript />;
case "py":
return <FaPython />;
case "css":
return <FaCss3 />;
case "json":
return <FaList />;
case "npmignore":
return <FaNpm />;
case "html":
return <FaHtml5 />;
case "md":
return <FaMarkdown />;
default:
return <FaFile />;
}
}

interface FolderIconProps {
isOpen: boolean;
}

function FolderIcon({ isOpen }: FolderIconProps): JSX.Element {
return isOpen ? (
<FaFolderOpen color="D9D3D0" className="icon" />
) : (
<FaFolder color="D9D3D0" className="icon" />
);
}

function Files(): JSX.Element | null {
const workspaceFolder = useSelector(
(state: RootState) => state.code.workspaceFolder,
);
const selectedIds = useSelector((state: RootState) => state.code.selectedIds);
const [explorerOpen, setExplorerOpen] = useState(true);
const workspaceTree = flattenTree(workspaceFolder);

useEffect(() => {
getWorkspace().then((file) => store.dispatch(updateWorkspace(file)));
}, []);

if (workspaceTree.length <= 1) {
return null;
}
if (!explorerOpen) {
return (
<div className="h-full bg-bg-workspace border-r-1 flex flex-col">
<div className="flex gap-1 border-b-1 p-1 justify-end">
<VscListTree
className="cursor-pointer"
onClick={() => setExplorerOpen(true)}
/>
</div>
</div>
);
}
return (
<div className="min-w-[250px] h-full bg-bg-workspace border-r-1 flex flex-col">
<div className="flex gap-1 border-b-1 p-1 justify-end">
<VscRefresh
onClick={() =>
getWorkspace().then((file) => store.dispatch(updateWorkspace(file)))
}
className="cursor-pointer"
/>
<VscClose
className="cursor-pointer"
onClick={() => setExplorerOpen(false)}
/>
</div>
<div className="w-full overflow-x-auto h-full py-2">
<TreeView
className="font-mono text-sm"
data={workspaceTree}
selectedIds={selectedIds}
expandedIds={workspaceTree.map((node) => node.id)}
onNodeSelect={(node) => {
if (!node.isBranch) {
let fullPath = node.element.name;
let currentNode = workspaceTree.find(
(file) => file.id === node.element.id,
);
while (currentNode !== undefined && currentNode.parent) {
currentNode = workspaceTree.find(
(file) => file.id === node.element.parent,
);
fullPath = `${currentNode!.name}/${fullPath}`;
}
selectFile(fullPath).then((code) => {
store.dispatch(setCode(code));
});
}
}}
// eslint-disable-next-line react/no-unstable-nested-components
nodeRenderer={({
element,
isBranch,
isExpanded,
getNodeProps,
level,
}) => (
<div
// eslint-disable-next-line react/jsx-props-no-spreading
{...getNodeProps()}
style={{ paddingLeft: 20 * (level - 1) }}
className="cursor-pointer nowrap flex items-center gap-2 aria-selected:bg-slate-500 hover:bg-slate-700"
>
<div className="shrink-0">
{isBranch ? (
<FolderIcon isOpen={isExpanded} />
) : (
<FileIcon filename={element.name} />
)}
</div>
{element.name}
</div>
)}
/>
</div>
</div>
);
}
import { RootState } from "../store";
import Files from "./Files";
import { cn } from "../utils/utils";

function CodeEditor(): JSX.Element {
const [selectedFileName, setSelectedFileName] = useState("welcome");
const [explorerOpen, setExplorerOpen] = useState(true);
const code = useSelector((state: RootState) => state.code.code);

const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-workspace")
.trim();

const handleEditorDidMount = (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
Expand All @@ -171,7 +22,7 @@ function CodeEditor(): JSX.Element {
inherit: true,
rules: [],
colors: {
"editor.background": bgColor,
"editor.background": "#171717",
},
});

Expand All @@ -180,16 +31,50 @@ function CodeEditor(): JSX.Element {
};

return (
<div className="w-full h-full bg-bg-workspace flex">
<Files />
<Editor
height="95%"
theme="vs-dark"
defaultLanguage="python"
defaultValue="# Welcome to OpenDevin!"
value={code}
onMount={handleEditorDidMount}
/>
<div
className={`${cn(
explorerOpen ? "grid-cols-[250px_auto]" : "grid-cols-[50px_auto]",
)} grid h-full bg-neutral-900 transition-all duration-500 ease-in-out`}
>
<div>
<Files
setSelectedFileName={setSelectedFileName}
setExplorerOpen={setExplorerOpen}
explorerOpen={explorerOpen}
/>
</div>
<div>
<Tabs
disableCursorAnimation
classNames={{
tabList:
"w-full relative rounded-none bg-neutral-900 p-0 border-r border-divider",
cursor: "w-full bg-neutral-600 rounded-none",
tab: "max-w-fit px-4 h-[36px]",
tabContent: "group-data-[selected=true]:text-neutral-50 ",
}}
aria-label="Options"
>
<Tab
key={
selectedFileName === ""
? "Welcome"
: selectedFileName.toLocaleLowerCase()
}
title={!selectedFileName ? "Welcome" : selectedFileName}
>
<div>
<Editor
height="100vh"
defaultLanguage="python"
defaultValue="# Welcome to OpenDevin!"
value={code}
onMount={handleEditorDidMount}
/>
</div>
</Tab>
</Tabs>
</div>
</div>
);
}
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/FileIcons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from "react";
import { DiJavascript } from "react-icons/di";
import {
FaCss3,
FaFile,
FaHtml5,
FaList,
FaMarkdown,
FaNpm,
FaPython,
} from "react-icons/fa";

interface FileIconProps {
filename: string;
}

function FileIcon({ filename }: FileIconProps): JSX.Element | null {
const extension = filename.slice(filename.lastIndexOf(".") + 1);
switch (extension) {
case "js":
return <DiJavascript />;
case "ts":
return <DiJavascript />;
case "py":
return <FaPython />;
case "css":
return <FaCss3 />;
case "json":
return <FaList />;
case "npmignore":
return <FaNpm />;
case "html":
return <FaHtml5 />;
case "md":
return <FaMarkdown />;
default:
return <FaFile />;
}
}

export default FileIcon;
Loading

0 comments on commit 6f795f5

Please sign in to comment.