Skip to content

Commit

Permalink
Merge commit '6e3b554317de7bc5d96ef81b4097287e05c0c4d0' into stateful…
Browse files Browse the repository at this point in the history
…-docker-via-sshd
  • Loading branch information
xingyaoww committed Apr 8, 2024
2 parents 3996093 + 6e3b554 commit 1b41176
Show file tree
Hide file tree
Showing 28 changed files with 434 additions and 423 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ run:
fi
@mkdir -p logs
@echo "$(YELLOW)Starting backend server...$(RESET)"
@poetry run nohup uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) > logs/backend_$(shell date +'%Y%m%d_%H%M%S').log 2>&1 &
@poetry run uvicorn opendevin.server.listen:app --port $(BACKEND_PORT) &
@echo "$(YELLOW)Waiting for the backend to start...$(RESET)"
@until nc -z localhost $(BACKEND_PORT); do sleep 0.1; done
@echo "$(GREEN)Backend started successfully.$(RESET)"
Expand Down
44 changes: 30 additions & 14 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import Errors from "./components/Errors";
import SettingModal from "./components/SettingModal";
import Terminal from "./components/Terminal";
import Workspace from "./components/Workspace";
import store, { RootState } from "./store";
import { setInitialized } from "./state/globalSlice";
import { fetchMsgTotal } from "./services/session";
import LoadMessageModal from "./components/LoadMessageModal";
import { ResFetchMsgTotal } from "./types/ResponseType";
import { ResConfigurations, ResFetchMsgTotal } from "./types/ResponseType";
import { fetchConfigurations, saveSettings } from "./services/settingsService";
import { RootState } from "./store";

interface Props {
setSettingOpen: (isOpen: boolean) => void;
Expand All @@ -30,22 +30,38 @@ function LeftNav({ setSettingOpen }: Props): JSX.Element {
);
}

// React.StrictMode will cause double rendering, use this to prevent it
let initOnce = false;

function App(): JSX.Element {
const { initialized } = useSelector((state: RootState) => state.global);
const [settingOpen, setSettingOpen] = useState(false);
const [loadMsgWarning, setLoadMsgWarning] = useState(false);
const settings = useSelector((state: RootState) => state.settings);

useEffect(() => {
if (!initialized) {
fetchMsgTotal()
.then((data: ResFetchMsgTotal) => {
if (data.msg_total > 0) {
setLoadMsgWarning(true);
}
store.dispatch(setInitialized(true));
})
.catch();
}
if (initOnce) return;
initOnce = true;
// only fetch configurations in the first time
fetchConfigurations()
.then((data: ResConfigurations) => {
saveSettings(
Object.fromEntries(
Object.entries(data).map(([key, value]) => [key, String(value)]),
),
Object.fromEntries(
Object.entries(settings).map(([key, value]) => [key, value]),
),
true,
);
})
.catch();
fetchMsgTotal()
.then((data: ResFetchMsgTotal) => {
if (data.msg_total > 0) {
setLoadMsgWarning(true);
}
})
.catch();
}, []);

const handleCloseModal = () => {
Expand Down
85 changes: 18 additions & 67 deletions frontend/src/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ import { useTypingEffect } from "../hooks/useTypingEffect";
import { I18nKey } from "../i18n/declaration";
import {
addAssistantMessageToChat,
setCurrentQueueMarkerState,
setCurrentTypingMsgState,
setTypingActive,
takeOneAndType,
} from "../services/chatService";
import { Message } from "../state/chatSlice";
import { RootState } from "../store";
Expand All @@ -29,25 +28,21 @@ interface IChatBubbleProps {
*
*/
function TypingChat() {
const { currentTypingMessage, currentQueueMarker, queuedTyping, messages } =
useSelector((state: RootState) => state.chat);
const { typeThis } = useSelector((state: RootState) => state.chat);

const messageContent = useTypingEffect([currentTypingMessage], {
const messageContent = useTypingEffect([typeThis?.content], {
loop: false,
setTypingActive,
setCurrentQueueMarkerState,
currentQueueMarker,
playbackRate: 0.1,
playbackRate: 0.099,
addAssistantMessageToChat,
assistantMessageObj: messages?.[queuedTyping[currentQueueMarker]],
takeOneAndType,
typeThis,
});

return (
currentQueueMarker !== null && (
<Card className="bg-success-100">
<CardBody>{messageContent}</CardBody>
</Card>
)
<Card className="bg-success-100">
<CardBody>{messageContent}</CardBody>
</Card>
);
}

Expand All @@ -72,14 +67,9 @@ function ChatBubble({ msg }: IChatBubbleProps): JSX.Element {

function MessageList(): JSX.Element {
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
messages,
queuedTyping,
typingActive,
currentQueueMarker,
currentTypingMessage,
newChatSequence,
} = useSelector((state: RootState) => state.chat);
const { typingActive, newChatSequence, typeThis } = useSelector(
(state: RootState) => state.chat,
);

const messageScroll = () => {
messagesEndRef.current?.scrollIntoView({
Expand All @@ -101,56 +91,17 @@ function MessageList(): JSX.Element {
}, [newChatSequence, typingActive]);

useEffect(() => {
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;

if (
currentQueueMarker !== null &&
currentQueueMarker !== 0 &&
currentTypingMessage !== newMessage
) {
setCurrentTypingMsgState(
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
);
}
}, [queuedTyping]);

useEffect(() => {
if (currentTypingMessage === "") return;
if (typeThis.content === "") return;

if (!typingActive) setTypingActive(true);
}, [currentTypingMessage]);

useEffect(() => {
const newMessage = messages?.[queuedTyping[currentQueueMarker]]?.content;
if (
newMessage &&
typingActive === false &&
currentTypingMessage !== newMessage
) {
if (currentQueueMarker !== 0) {
setCurrentTypingMsgState(
messages?.[queuedTyping?.[currentQueueMarker]]?.content,
);
}
}
}, [typingActive]);

useEffect(() => {
if (currentQueueMarker === 0) {
setCurrentTypingMsgState(messages?.[queuedTyping?.[0]]?.content);
}
}, [currentQueueMarker]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [typeThis]);

return (
<div className="flex-1 overflow-y-auto">
{newChatSequence.map((msg, index) =>
// eslint-disable-next-line no-nested-ternary
msg.sender === "user" || msg.sender === "assistant" ? (
<ChatBubble key={index} msg={msg} />
) : (
<div key={index} />
),
)}
{newChatSequence.map((msg, index) => (
<ChatBubble key={index} msg={msg} />
))}

{typingActive && (
<div className="flex mb-2.5 pr-5 pl-5 bg-s">
Expand Down
67 changes: 35 additions & 32 deletions frontend/src/components/SettingModal.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import {
Autocomplete,
AutocompleteItem,
Button,
Input,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalContent,
ModalFooter,
Input,
Button,
Autocomplete,
AutocompleteItem,
ModalHeader,
Select,
SelectItem,
} from "@nextui-org/react";
import { KeyboardEvent } from "@react-types/shared/src/events";
import { useTranslation } from "react-i18next";
import {
INITIAL_AGENTS,
fetchModels,
fetchAgents,
fetchModels,
INITIAL_AGENTS,
INITIAL_MODELS,
saveSettings,
getInitialModel,
} from "../services/settingsService";
import { RootState } from "../store";
import { I18nKey } from "../i18n/declaration";
import { AvailableLanguages } from "../i18n";
import ArgConfigType from "../types/ConfigType";

interface Props {
isOpen: boolean;
Expand All @@ -39,21 +39,17 @@ const cachedAgents = JSON.parse(
localStorage.getItem("supportedAgents") || "[]",
);

function SettingModal({ isOpen, onClose }: Props): JSX.Element {
const defModel = useSelector((state: RootState) => state.settings.model);
const [model, setModel] = useState(defModel);
const defAgent = useSelector((state: RootState) => state.settings.agent);
const [agent, setAgent] = useState(defAgent);
const defWorkspaceDirectory = useSelector(
(state: RootState) => state.settings.workspaceDirectory,
function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
const settings = useSelector((state: RootState) => state.settings);
const [model, setModel] = useState(settings[ArgConfigType.LLM_MODEL]);
const [inputModel, setInputModel] = useState(
settings[ArgConfigType.LLM_MODEL],
);
const [agent, setAgent] = useState(settings[ArgConfigType.AGENT]);
const [workspaceDirectory, setWorkspaceDirectory] = useState(
defWorkspaceDirectory,
settings[ArgConfigType.WORKSPACE_DIR],
);
const defLanguage = useSelector(
(state: RootState) => state.settings.language,
);
const [language, setLanguage] = useState(defLanguage);
const [language, setLanguage] = useState(settings[ArgConfigType.LANGUAGE]);

const { t } = useTranslation();

Expand All @@ -65,12 +61,6 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
);

useEffect(() => {
getInitialModel()
.then((initialModel) => {
setModel(initialModel);
})
.catch();

fetchModels().then((fetchedModels) => {
const sortedModels = fetchedModels.sort(); // Sorting the models alphabetically
setSupportedModels(sortedModels);
Expand All @@ -85,10 +75,16 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {

const handleSaveCfg = () => {
saveSettings(
{ model, agent, workspaceDirectory, language },
model !== defModel &&
agent !== defAgent &&
workspaceDirectory !== defWorkspaceDirectory,
{
[ArgConfigType.LLM_MODEL]: model ?? inputModel,
[ArgConfigType.AGENT]: agent,
[ArgConfigType.WORKSPACE_DIR]: workspaceDirectory,
[ArgConfigType.LANGUAGE]: language,
},
Object.fromEntries(
Object.entries(settings).map(([key, value]) => [key, value]),
),
false,
);
onClose();
};
Expand Down Expand Up @@ -127,9 +123,10 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
onSelectionChange={(key) => {
setModel(key as string);
}}
onInputChange={(e) => setInputModel(e)}
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
defaultFilter={customFilter}
defaultInputValue={model}
defaultInputValue={inputModel}
allowsCustomValue
>
{(item: { label: string; value: string }) => (
Expand Down Expand Up @@ -187,4 +184,10 @@ function SettingModal({ isOpen, onClose }: Props): JSX.Element {
);
}

function SettingModal({ isOpen, onClose }: Props): JSX.Element {
// Do not render the modal if it is not open, prevents reading empty from localStorage after initialization
if (!isOpen) return <div />;
return <InnerSettingModal isOpen={isOpen} onClose={onClose} />;
}

export default SettingModal;
19 changes: 9 additions & 10 deletions frontend/src/hooks/useTypingEffect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,23 @@ export const useTypingEffect = (
loop = false,
playbackRate = 0.1,
setTypingActive = () => {},
setCurrentQueueMarkerState = () => {},
currentQueueMarker = 0,
addAssistantMessageToChat = () => {},
assistantMessageObj = { content: "", sender: "assistant" },
takeOneAndType = () => {},
typeThis = { content: "", sender: "assistant" },
}: {
loop?: boolean;
playbackRate?: number;
setTypingActive?: (bool: boolean) => void;
setCurrentQueueMarkerState?: (marker: number) => void;
currentQueueMarker?: number;
addAssistantMessageToChat?: (msg: Message) => void;
assistantMessageObj?: Message;
takeOneAndType?: () => void;
typeThis?: Message;
} = {
loop: false,
playbackRate: 0.1,
setTypingActive: () => {},
currentQueueMarker: 0,
addAssistantMessageToChat: () => {},
assistantMessageObj: { content: "", sender: "assistant" },
takeOneAndType: () => {},
typeThis: { content: "", sender: "assistant" },
},
) => {
// eslint-disable-next-line prefer-const
Expand All @@ -50,8 +48,8 @@ export const useTypingEffect = (
if (stringIndex === strings.length) {
if (!loop) {
setTypingActive(false);
setCurrentQueueMarkerState(currentQueueMarker + 1);
addAssistantMessageToChat(assistantMessageObj);
addAssistantMessageToChat(typeThis);
takeOneAndType();
return;
}
stringIndex = 0;
Expand All @@ -73,6 +71,7 @@ export const useTypingEffect = (
return () => {
window.clearTimeout(timeoutId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const nonBreakingSpace = "\u00A0";
Expand Down
Loading

0 comments on commit 1b41176

Please sign in to comment.