mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
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:
parent
87d56d961f
commit
dab86c4a77
2868
frontend/package-lock.json
generated
2868
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
22
frontend/src/assets/calendar.tsx
Normal file
22
frontend/src/assets/calendar.tsx
Normal 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;
|
||||
22
frontend/src/assets/cmd-line.tsx
Normal file
22
frontend/src/assets/cmd-line.tsx
Normal 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;
|
||||
27
frontend/src/assets/cog-tooth.tsx
Normal file
27
frontend/src/assets/cog-tooth.tsx
Normal 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;
|
||||
22
frontend/src/assets/earth.tsx
Normal file
22
frontend/src/assets/earth.tsx
Normal 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;
|
||||
22
frontend/src/assets/pencil.tsx
Normal file
22
frontend/src/assets/pencil.tsx
Normal 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;
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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'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's
|
||||
instructions.
|
||||
<span>
|
||||
Set up the development environment according to the project's
|
||||
instructions.
|
||||
</span>
|
||||
</li>
|
||||
{/* Add more tasks */}
|
||||
</ul>
|
||||
|
||||
147
frontend/src/components/SettingModal.tsx
Normal file
147
frontend/src/components/SettingModal.tsx
Normal 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;
|
||||
@ -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;
|
||||
|
||||
71
frontend/src/components/Workspace.tsx
Normal file
71
frontend/src/components/Workspace.tsx
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
.planner {
|
||||
background: black;
|
||||
padding: 1rem;
|
||||
height: 90%;
|
||||
margin: 1rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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>,
|
||||
);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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";
|
||||
|
||||
21
frontend/src/types/TabOption.tsx
Normal file
21
frontend/src/types/TabOption.tsx
Normal 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 };
|
||||
@ -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"
|
||||
})],
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user