Style improve fe layout and interaction (#385)

* style: improve FE layout and interaction, remove daisyui, switch to nextui; reduce the props of Workspace component; adjust style of chat bubble and workspace tabs.
This commit is contained in:
iFurySt 2024-03-31 12:52:34 +08:00 committed by GitHub
parent 87d56d961f
commit dab86c4a77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 3235 additions and 596 deletions

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,8 @@
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.2.10",
"@react-types/shared": "^3.22.1",
"@reduxjs/toolkit": "^2.2.2",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
@ -20,6 +22,7 @@
"@vitejs/plugin-react": "^4.2.1",
"@xterm/xterm": "^5.4.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^11.0.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
@ -66,7 +69,6 @@
"@types/jest": "^29.5.12",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19",
"daisyui": "^4.9.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^9.1.0",

View File

@ -3,52 +3,6 @@
@tailwind components;
@tailwind utilities;
.app {
height: 100vh;
background-color: #1e1e1e;
color: #fff;
}
.padded-app {
padding: 2px;
}
.left-pane {
flex: 1;
background-color: #252526;
margin: 1rem;
overflow: hidden;
border-radius: 1rem;
}
.right-pane {
flex: 1;
display: flex;
flex-direction: column;
border-radius: 1rem;
margin: 1rem;
background-color: #ffffff24;
overflow: hidden;
}
.workspace-content {
flex: 1;
display: flex;
flex-direction: column;
}
.workspace-heading {
padding: 0 1rem;
display: flex;
margin: 1rem 0;
justify-content: space-between;
font-size: 30px;
font-weight: bolder;
}
.tab-container {
display: flex;
background-color: #333333;
}
.tab {
padding: 10px 20px;
cursor: pointer;
@ -62,11 +16,3 @@
font-weight: bolder;
}
.tab-content {
flex: 1;
height: 95%;
border-radius: 0 0 0.5rem 0.5rem;
}
.xterm-screen {
padding: 10px 0 0 10px;
}

View File

@ -1,85 +1,28 @@
import React, { useState } from "react";
import "./App.css";
import ChatInterface from "./components/ChatInterface";
import Terminal from "./components/Terminal";
import Planner from "./components/Planner";
import CodeEditor from "./components/CodeEditor";
import Browser from "./components/Browser";
import Errors from "./components/Errors";
import BannerSettings from "./components/BannerSettings";
const TAB_OPTIONS = ["terminal", "planner", "code", "browser"] as const;
type TabOption = (typeof TAB_OPTIONS)[number];
type TabProps = {
name: string;
active: boolean;
onClick: () => void;
};
function Tab({ name, active, onClick }: TabProps): JSX.Element {
return (
<div
className={`tab ${active ? "tab-active" : ""}`}
onClick={() => onClick()}
>
<p className="font-bold">{name}</p>
</div>
);
}
const tabData = {
terminal: {
name: "Terminal",
component: null,
},
planner: {
name: "Planner",
component: <Planner key="planner" />,
},
code: {
name: "Code Editor",
component: <CodeEditor key="code" />,
},
browser: {
name: "Browser",
component: <Browser key="browser" />,
},
};
import SettingModal from "./components/SettingModal";
import Workspace from "./components/Workspace";
function App(): JSX.Element {
const [activeTab, setActiveTab] = useState<TabOption>("terminal");
const [settingOpen, setSettingOpen] = useState(false);
const handleCloseModal = () => {
setSettingOpen(false);
};
return (
<div className="app flex">
<div className="flex h-screen bg-bg-dark text-white">
<Errors />
<div className="left-pane">
<ChatInterface />
<div className="flex-1 rounded-xl m-4 overflow-hidden bg-bg-light">
<ChatInterface setSettingOpen={setSettingOpen} />
</div>
<div className="right-pane">
<div className="navbar bg-base-100">
<div className="flex-1">
<div className="btn btn-ghost text-xl xl:w-full xl:h-full h-1/2 w-1/2 ml-4">
OpenDevin Workspace
</div>
</div>
<div className="flex">
<BannerSettings />
</div>
</div>
<div role="tablist" className="tabs tabs-bordered tabs-lg bg-base-100">
{TAB_OPTIONS.map((tab) => (
<Tab
key={tab}
name={tabData[tab].name}
active={activeTab === tab}
onClick={() => setActiveTab(tab)}
/>
))}
</div>
{/* Keep terminal permanently open - see component for more details */}
<Terminal key="terminal" hidden={activeTab !== "terminal"} />
{tabData[activeTab].component}
<div className="flex flex-col flex-1 m-4 overflow-hidden rounded-xl bg-bg-light">
<Workspace />
</div>
<SettingModal isOpen={settingOpen} onClose={handleCloseModal} />
</div>
);
}

View File

@ -0,0 +1,22 @@
import React from "react";
function Calendar() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z"
/>
</svg>
);
}
export default Calendar;

View File

@ -0,0 +1,22 @@
import React from "react";
function CmdLine() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
);
}
export default CmdLine;

View File

@ -0,0 +1,27 @@
import React from "react";
function CogTooth(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
);
}
export default CogTooth;

View File

@ -0,0 +1,22 @@
import React from "react";
function Earth() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
);
}
export default Earth;

View File

@ -0,0 +1,22 @@
import React from "react";
function Pencil() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
);
}
export default Pencil;

View File

@ -1,64 +0,0 @@
import React, { ChangeEvent, useEffect, useState } from "react";
import {
INITIAL_AGENTS,
INITIAL_MODELS,
changeAgent,
changeModel,
fetchAgents,
fetchModels,
} from "../services/settingsService";
import "./css/BannerSettings.css";
function ModelSelect(): JSX.Element {
const [models, setModels] = useState<string[]>(INITIAL_MODELS);
useEffect(() => {
fetchModels().then((fetchedModels) => {
setModels(fetchedModels);
});
}, []);
return (
<select
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
changeModel(e.target.value)
}
className="select max-w-xs bg-base-300 xl:w-full w-1/3"
>
{models.map((model) => (
<option key={model}>{model}</option>
))}
</select>
);
}
function AgentSelect(): JSX.Element {
const [agents, setAgents] = useState<string[]>(INITIAL_AGENTS);
useEffect(() => {
fetchAgents().then((fetchedAgents) => {
setAgents(fetchedAgents);
});
}, []);
return (
<select
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
changeAgent(e.target.value)
}
className="select max-w-xs bg-base-300 xl:w-full w-1/3"
>
{agents.map((agent) => (
<option key={agent}>{agent}</option>
))}
</select>
);
}
function BannerSettings(): JSX.Element {
return (
<div className="banner">
<ModelSelect />
<AgentSelect />
</div>
);
}
export default BannerSettings;

View File

@ -1,12 +1,11 @@
import React from "react";
import "./css/Browser.css";
import { useSelector } from "react-redux";
import { RootState } from "../store";
function Browser(): JSX.Element {
const url = useSelector((state: RootState) => state.browser.url);
return (
<div className="mockup-browser">
<div className="h-full m-2 bg-bg-workspace mockup-browser">
<div className="mockup-browser-toolbar">
<div className="input">{url}</div>
</div>

View File

@ -1,11 +1,11 @@
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { Card, CardBody } from "@nextui-org/react";
import assistantAvatar from "../assets/assistant-avatar.png";
import userAvatar from "../assets/user-avatar.png";
import { sendChatMessage } from "../services/chatService";
import { RootState } from "../store";
import "./css/ChatInterface.css";
import { changeDirectory as sendChangeDirectorySocketMessage } from "../services/settingsService";
import CogTooth from "../assets/cog-tooth";
function MessageList(): JSX.Element {
const messagesEndRef = useRef<HTMLDivElement>(null);
@ -16,18 +16,20 @@ function MessageList(): JSX.Element {
}, [messages]);
return (
<div className="message-list">
<div className="flex-1 overflow-y-auto">
{messages.map((msg, index) => (
<div key={index} className="message-layout">
<div key={index} className="flex mb-2.5">
<div
className={`${msg.sender === "user" ? "user-message" : "message"}`}
className={`${msg.sender === "user" ? "flex flex-row-reverse mt-2.5 mr-2.5 mb-0 ml-auto" : "flex"}`}
>
<img
src={msg.sender === "user" ? userAvatar : assistantAvatar}
alt={`${msg.sender} avatar`}
className="avatar"
className="w-[40px] h-[40px] mx-2.5"
/>
<div className="chat chat-bubble">{msg.content}</div>
<Card className="w-4/5">
<CardBody>{msg.content}</CardBody>
</Card>
</div>
</div>
))}
@ -38,51 +40,22 @@ function MessageList(): JSX.Element {
function InitializingStatus(): JSX.Element {
return (
<div className="initializing-status">
<img src={assistantAvatar} alt="assistant avatar" className="avatar" />
<div className="flex items-center m-auto h-full">
<img
src={assistantAvatar}
alt="assistant avatar"
className="w-[40px] h-[40px] mx-2.5"
/>
<div>Initializing agent (may take up to 10 seconds)...</div>
</div>
);
}
function DirectoryInput(): JSX.Element {
const [editing, setEditing] = useState(false);
const [directory, setDirectory] = useState("Default");
function save() {
setEditing(false);
sendChangeDirectorySocketMessage(directory);
}
function onDirectoryInputChange(e: ChangeEvent<HTMLInputElement>) {
setEditing(true);
setDirectory(e.target.value);
}
return (
<div className="flex p-2 justify-center gap-2 bg-neutral-700">
<label htmlFor="directory-input" className="label">
Directory
</label>
<input
type="text"
className="input"
id="directory-input"
placeholder="Default"
onChange={onDirectoryInputChange}
/>
<button
type="button"
className={`btn ${editing ? "" : "hidden"}`}
onClick={save}
>
Save
</button>
</div>
);
interface Props {
setSettingOpen: (isOpen: boolean) => void;
}
function ChatInterface(): JSX.Element {
function ChatInterface({ setSettingOpen }: Props): JSX.Element {
const { initialized } = useSelector((state: RootState) => state.task);
const [inputMessage, setInputMessage] = useState("");
@ -94,13 +67,22 @@ function ChatInterface(): JSX.Element {
};
return (
<div className="chat-interface">
<DirectoryInput />
<div className="flex flex-col h-full p-0 bg-bg-light">
<div className="w-full flex justify-between p-5">
<div />
<div
className="cursor-pointer hover:opacity-80"
onClick={() => setSettingOpen(true)}
>
<CogTooth />
</div>
</div>
{initialized ? <MessageList /> : <InitializingStatus />}
<div className="input-container">
<div className="input-box">
<div className="w-full flex items-center p-5 rounded-none rounded-bl-lg rounded-br-lg">
<div className="w-full flex items-center rounded-xl text-base bg-bg-input">
<input
type="text"
className="flex-1 py-4 px-2.5 border-none mx-4 bg-bg-input text-white outline-none"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder="Send a message (won't interrupt the Assistant)"
@ -112,10 +94,11 @@ function ChatInterface(): JSX.Element {
/>
<button
type="button"
className="bg-transparent border-none rounded py-2.5 px-5 hover:opacity-80 cursor-pointer select-none"
onClick={handleSendMessage}
disabled={!initialized}
>
<span className="button-text">Send</span>
Send
</button>
</div>
</div>

View File

@ -1,26 +1,43 @@
import React from "react";
import Editor from "@monaco-editor/react";
import Editor, { Monaco } from "@monaco-editor/react";
import { useSelector } from "react-redux";
import type { editor } from "monaco-editor";
import { RootState } from "../store";
function CodeEditor(): JSX.Element {
const code = useSelector((state: RootState) => state.code.code);
const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-workspace")
.trim();
const handleEditorDidMount = (
editor: editor.IStandaloneCodeEditor,
monaco: Monaco,
) => {
// 定义一个自定义主题
monaco.editor.defineTheme("my-theme", {
base: "vs-dark",
inherit: true,
rules: [],
colors: {
"editor.background": bgColor,
},
});
// 应用自定义主题
monaco.editor.setTheme("my-theme");
};
return (
<div
className="editor"
style={{
height: "100%",
margin: "1rem",
borderRadius: "1rem",
}}
>
<div className="w-full h-full bg-bg-workspace">
<Editor
height="95%"
theme="vs-dark"
defaultLanguage="python"
defaultValue="# Welcome to OpenDevin!"
value={code}
onMount={handleEditorDidMount}
/>
</div>
);

View File

@ -1,27 +1,32 @@
import React from "react";
import "./css/Planner.css";
function Planner(): JSX.Element {
return (
<div className="planner">
<div className="h-full w-full bg-bg-workspace">
<h3>
Current Focus: Set up the development environment according to the
project&apos;s instructions.
</h3>
<ul>
<li>
<ul className="ml-4 mt-3">
<li className="space-x-2">
<input type="checkbox" checked readOnly />
Clone the repository and review the README for project setup
instructions.
<span>
Clone the repository and review the README for project setup
instructions.
</span>
</li>
<li>
<li className="space-x-2">
<input type="checkbox" checked readOnly />
Identify the package manager and install necessary dependencies.
<span>
Identify the package manager and install necessary dependencies.
</span>
</li>
<li>
<li className="space-x-2">
<input type="checkbox" />
Set up the development environment according to the project&apos;s
instructions.
<span>
Set up the development environment according to the project&apos;s
instructions.
</span>
</li>
{/* Add more tasks */}
</ul>

View File

@ -0,0 +1,147 @@
import React, { useEffect, useState } from "react";
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Input,
Button,
Autocomplete,
AutocompleteItem,
} from "@nextui-org/react";
import { KeyboardEvent } from "@react-types/shared/src/events";
import {
INITIAL_AGENTS,
changeAgent,
changeDirectory as sendChangeDirectorySocketMessage,
changeModel,
fetchModels,
INITIAL_MODELS,
} from "../services/settingsService";
interface Props {
isOpen: boolean;
onClose: () => void;
}
const cachedModels = JSON.parse(
localStorage.getItem("supportedModels") || "[]",
);
const cachedAgents = JSON.parse(
localStorage.getItem("supportedAgents") || "[]",
);
function SettingModal({ isOpen, onClose }: Props): JSX.Element {
const [workspaceDirectory, setWorkspaceDirectory] = useState(
localStorage.getItem("workspaceDirectory") || "./workspace",
);
const [model, setModel] = useState(
localStorage.getItem("model") || "gpt-3.5-turbo-1106",
);
const [supportedModels, setSupportedModels] = useState(
cachedModels.length > 0 ? cachedModels : INITIAL_MODELS,
);
const [agent, setAgent] = useState(
localStorage.getItem("agent") || "LangchainsAgent",
);
const [supportedAgents] = useState(
cachedAgents.length > 0 ? cachedAgents : INITIAL_AGENTS,
);
useEffect(() => {
fetchModels().then((fetchedModels) => {
setSupportedModels(fetchedModels);
localStorage.setItem("supportedModels", JSON.stringify(fetchedModels));
});
}, []);
const handleSaveCfg = () => {
sendChangeDirectorySocketMessage(workspaceDirectory);
changeModel(model);
changeAgent(agent);
localStorage.setItem("model", model);
localStorage.setItem("workspaceDirectory", workspaceDirectory);
localStorage.setItem("agent", agent);
onClose();
};
const customFilter = (item: string, input: string) =>
item.toLowerCase().includes(input.toLowerCase());
return (
<Modal isOpen={isOpen} onClose={onClose} hideCloseButton backdrop="blur">
<ModalContent>
<>
<ModalHeader className="flex flex-col gap-1">
Configuration
</ModalHeader>
<ModalBody>
<Input
type="text"
label="OpenDevin Workspace Directory"
defaultValue={workspaceDirectory}
placeholder="Default: ./workspace"
onChange={(e) => setWorkspaceDirectory(e.target.value)}
/>
<Autocomplete
defaultItems={supportedModels.map((v: string) => ({
label: v,
value: v,
}))}
label="Model"
placeholder="Select a model"
defaultSelectedKey={model}
// className="max-w-xs"
onSelectionChange={(key) => {
setModel(key as string);
}}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
<Autocomplete
defaultItems={supportedAgents.map((v: string) => ({
label: v,
value: v,
}))}
label="Agent"
placeholder="Select a agent"
defaultSelectedKey={agent}
// className="max-w-xs"
onSelectionChange={(key) => {
setAgent(key as string);
}}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
>
{(item: { label: string; value: string }) => (
<AutocompleteItem key={item.value} value={item.value}>
{item.label}
</AutocompleteItem>
)}
</Autocomplete>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button color="primary" onPress={handleSaveCfg}>
Save
</Button>
</ModalFooter>
</>
</ModalContent>
</Modal>
);
}
export default SettingModal;

View File

@ -41,19 +41,19 @@ class JsonWebsocketAddon {
}
}
type TerminalProps = {
hidden: boolean;
};
/**
* The terminal's content is set by write messages. To avoid complicated state logic,
* we keep the terminal persistently open as a child of <App /> and hidden when not in use.
*/
function Terminal({ hidden }: TerminalProps): JSX.Element {
function Terminal(): JSX.Element {
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const bgColor = getComputedStyle(document.documentElement)
.getPropertyValue("--bg-workspace")
.trim();
const terminal = new XtermTerminal({
// This value is set to the appropriate value by the
// `fitAddon.fit()` call below.
@ -63,6 +63,9 @@ function Terminal({ hidden }: TerminalProps): JSX.Element {
cols: 0,
fontFamily: "Menlo, Monaco, 'Courier New', monospace",
fontSize: 14,
theme: {
background: bgColor,
},
});
terminal.write("$ ");
@ -85,17 +88,7 @@ function Terminal({ hidden }: TerminalProps): JSX.Element {
};
}, []);
return (
<div
ref={terminalRef}
style={{
width: "100%",
height: "100%",
display: hidden ? "none" : "block",
padding: "1rem",
}}
/>
);
return <div ref={terminalRef} className="h-full w-full block" />;
}
export default Terminal;

View File

@ -0,0 +1,71 @@
import React, { useState } from "react";
import { Tab, Tabs } from "@nextui-org/react";
import Terminal from "./Terminal";
import Planner from "./Planner";
import CodeEditor from "./CodeEditor";
import Browser from "./Browser";
import { TabType, TabOption, AllTabs } from "../types/TabOption";
import CmdLine from "../assets/cmd-line";
import Calendar from "../assets/calendar";
import Earth from "../assets/earth";
import Pencil from "../assets/pencil";
const tabData = {
[TabOption.TERMINAL]: {
name: "Terminal",
icon: <CmdLine />,
component: <Terminal key="terminal" />,
},
[TabOption.PLANNER]: {
name: "Planner",
icon: <Calendar />,
component: <Planner key="planner" />,
},
[TabOption.CODE]: {
name: "Code Editor",
icon: <Pencil />,
component: <CodeEditor key="code" />,
},
[TabOption.BROWSER]: {
name: "Browser",
icon: <Earth />,
component: <Browser key="browser" />,
},
};
function Workspace() {
const [activeTab, setActiveTab] = useState<TabType>(TabOption.TERMINAL);
return (
<>
<div className="w-full p-4 text-2xl font-bold select-none">
OpenDevin Workspace
</div>
<div role="tablist" className="tabs tabs-bordered tabs-lg ">
<Tabs
variant="underlined"
size="lg"
onSelectionChange={(v) => {
setActiveTab(v as TabType);
}}
>
{AllTabs.map((tab) => (
<Tab
key={tab}
title={
<div className="flex items-center space-x-2">
{tabData[tab].icon}
<span>{tabData[tab].name}</span>
</div>
}
/>
))}
</Tabs>
</div>
<div className="h-full w-full p-4 bg-bg-workspace">
{tabData[activeTab].component}
</div>
</>
);
}
export default Workspace;

View File

@ -1,13 +0,0 @@
.banner {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}
select {
padding: 0.5rem;
border: 0;
border-radius: 5px;
font-size: 1rem;
}

View File

@ -1,36 +0,0 @@
.mockup-browser {
background: black;
padding: 1rem;
height: 90%;
margin: 1rem;
border-radius: 1rem;
}
.url {
margin: 0.5rem;
padding: 0.5rem 1rem;
background-color: white;
border-radius: 2rem;
color: #252526;
}
.browser {
height: 90%;
width: 100%;
border-radius: 2rem;
padding: 1rem;
}
.url {
margin: 0.5rem;
padding: 0.5rem 1rem;
width: 80%;
background-color: white;
border-radius: 2rem;
color: #252526;
}
.screenshot {
width: 100%;
height: 96%;
max-height: calc(90vh - 100px); /* 100px is the height of the header */
object-fit: cover;
}

View File

@ -1,125 +0,0 @@
.chat-interface {
display: flex;
flex-direction: column;
height: 100%;
padding: 0px;
background-color: #252526;
}
.initializing-status {
display: flex;
align-items: center;
margin: auto;
height: 100%;
}
.message-list {
flex: 1;
overflow-y: auto;
padding-top: 1rem;
}
.message-layout {
display: flex;
margin-bottom: 10px;
}
.message {
display: flex;
}
.user-message {
display: flex;
flex-direction: row-reverse;
margin: 10px 10px 0 auto;
}
.user-message .message-content {
background-color: #007acc;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 10px;
}
.message-content {
background-color: #333333;
color: #fff;
padding: 10px;
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.input-container {
width: 100%;
display: flex;
align-items: center;
background-color: #3e3e3e;
padding: 10px;
border-radius: 0 0rem 1rem 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
color: #fff;
}
.button-text {
font-size: 16px;
}
.input-box {
border-radius: 1rem;
background-color: #333333;
width: 100%;
display: flex;
}
.input-box input {
flex: 1;
padding: 10px;
border: none;
border-radius: 5px;
margin: 0 10px;
background-color: #3c3c3c;
color: #fff;
background-color: transparent;
outline: transparent;
}
.input-box button {
background-color: transparent;
padding: 10px 20px;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.input-box button:hover {
background-color: transparent;
}
.custom-file-input {
display: inline-block;
padding: 10px 20px;
font-size: 16px;
font-weight: bold;
color: white;
background-color: rgb(62, 62, 62);
border: none;
border-radius: 5px;
cursor: pointer;
}
.custom-file-input:hover {
background-color: rgb(70, 70, 70);
}
.custom-file-input input {
display: none;
}
.selected-directory {
display: flex;
justify-content: space-between;
padding: 10px 20px;
background-color: rgb(62, 62, 62);
}
.selected-directory > button:hover {
text-decoration: underline;
}

View File

@ -1,7 +0,0 @@
.planner {
background: black;
padding: 1rem;
height: 90%;
margin: 1rem;
border-radius: 1rem;
}

View File

@ -1,3 +1,12 @@
:root {
--bg-dark: #1e1e1e;
--bg-light: #292929;
--bg-input: #393939;
--bg-workspace: #171717;
background-color: var(--bg-dark) !important;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@ -1,7 +1,9 @@
import React from "react";
// import React from "react";
import * as React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { Provider } from "react-redux";
import { NextUIProvider } from "@nextui-org/react";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import store from "./store";
@ -12,7 +14,9 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
<NextUIProvider>
<App />
</NextUIProvider>
</Provider>
</React.StrictMode>,
);

View File

@ -1,5 +1,5 @@
import store from "../store";
import { ActionMessage } from "./types/Message";
import { ActionMessage } from "../types/Message";
import { setScreenshotSrc, setUrl } from "../state/browserSlice";
import { appendAssistantMessage } from "../state/chatSlice";
import { setCode } from "../state/codeSlice";

View File

@ -1,6 +1,6 @@
import { appendAssistantMessage } from "../state/chatSlice";
import store from "../store";
import { ObservationMessage } from "./types/Message";
import { ObservationMessage } from "../types/Message";
export function handleObservationMessage(message: ObservationMessage) {
store.dispatch(appendAssistantMessage(message.message));

View File

@ -1,5 +1,5 @@
import store from "../store";
import { ActionMessage, ObservationMessage } from "./types/Message";
import { ActionMessage, ObservationMessage } from "../types/Message";
import { appendError } from "../state/errorsSlice";
import { handleActionMessage } from "./actions";
import { handleObservationMessage } from "./observations";

View File

@ -0,0 +1,21 @@
enum TabOption {
TERMINAL = "terminal",
PLANNER = "planner",
CODE = "code",
BROWSER = "browser",
}
type TabType =
| TabOption.TERMINAL
| TabOption.PLANNER
| TabOption.CODE
| TabOption.BROWSER;
const AllTabs = [
TabOption.TERMINAL,
TabOption.PLANNER,
TabOption.CODE,
TabOption.BROWSER,
];
export { AllTabs, TabOption, type TabType };

View File

@ -1,12 +1,23 @@
/** @type {import('tailwindcss').Config} */
const {nextui} = require("@nextui-org/react");
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
content: [
'./src/**/*.{js,ts,jsx,tsx}',
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {},
extend: {
colors: {
'bg-dark': 'var(--bg-dark)',
'bg-light': 'var(--bg-light)',
'bg-input': 'var(--bg-input)',
'bg-workspace': 'var(--bg-workspace)'
},
},
},
daisyui: {
themes: ["dark"],
},
plugins: [require('daisyui')],
darkMode: "class",
plugins: [nextui({
defaultTheme: "dark"
})],
}