mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix(): build out opendevin modal component (#1141)
This commit is contained in:
parent
516c9bf1e0
commit
7e825b571f
@ -1,77 +1,82 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { fetchMsgs, clearMsgs } from "../services/session";
|
||||
import { sendChatMessageFromEvent } from "../services/chatService";
|
||||
import { handleAssistantMessage } from "../services/actions";
|
||||
import { ResFetchMsg } from "../types/ResponseType";
|
||||
import ODModal from "./ODModal";
|
||||
import toast from "../utils/toast";
|
||||
|
||||
interface Props {
|
||||
interface LoadMessageModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function LoadMessageModal({ isOpen, onClose }: Props): JSX.Element {
|
||||
const handleDelMsg = () => {
|
||||
function LoadMessageModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
}: LoadMessageModalProps): JSX.Element {
|
||||
const handleStartNewSession = () => {
|
||||
clearMsgs().then().catch();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleLoadMsg = () => {
|
||||
fetchMsgs()
|
||||
.then((data) => {
|
||||
if (
|
||||
data === undefined ||
|
||||
data.messages === undefined ||
|
||||
data.messages.length === 0
|
||||
) {
|
||||
return;
|
||||
const handleResumeSession = async () => {
|
||||
try {
|
||||
const data = await fetchMsgs();
|
||||
if (!data || !data.messages || data.messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.messages.forEach((msg: ResFetchMsg) => {
|
||||
switch (msg.role) {
|
||||
case "user":
|
||||
sendChatMessageFromEvent(msg.payload);
|
||||
break;
|
||||
case "assistant":
|
||||
handleAssistantMessage(msg.payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const { messages } = data;
|
||||
messages.forEach((msg: ResFetchMsg) => {
|
||||
switch (msg.role) {
|
||||
case "user":
|
||||
sendChatMessageFromEvent(msg.payload);
|
||||
break;
|
||||
case "assistant":
|
||||
handleAssistantMessage(msg.payload);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch();
|
||||
onClose();
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast.stickyError("ws", "Error fetching the session");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} hideCloseButton backdrop="blur">
|
||||
<ModalContent>
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
Unfinished Session Detected
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
You have an unfinished session. Do you want to load it?
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={handleDelMsg}>
|
||||
No, start a new session
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleLoadMsg}>
|
||||
Okay, load it
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ODModal
|
||||
size="md"
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
hideCloseButton
|
||||
backdrop="blur"
|
||||
title="Unfinished Session Detected"
|
||||
primaryAction={
|
||||
<Button
|
||||
className="bg-primary rounded-small"
|
||||
onPress={handleResumeSession}
|
||||
>
|
||||
Resume Session
|
||||
</Button>
|
||||
}
|
||||
secondaryAction={
|
||||
<Button
|
||||
className="bg-neutral-500 rounded-small"
|
||||
onPress={handleStartNewSession}
|
||||
>
|
||||
Start New Session
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
You seem to have an unfinished task. Would you like to pick up where you
|
||||
left off or start fresh?
|
||||
</p>
|
||||
</ODModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
71
frontend/src/components/ODModal.tsx
Normal file
71
frontend/src/components/ODModal.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ModalProps,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from "@nextui-org/react";
|
||||
|
||||
interface ODModalProps extends Omit<ModalProps, "children"> {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
primaryAction?: React.ReactNode;
|
||||
secondaryAction?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
size: "sm" | "md";
|
||||
}
|
||||
|
||||
function ODModal(props: ODModalProps): React.ReactElement {
|
||||
const {
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
primaryAction,
|
||||
secondaryAction,
|
||||
size,
|
||||
...modalProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="bg-neutral-900 rounded-large"
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...modalProps}
|
||||
>
|
||||
<ModalContent
|
||||
className={`${size === "sm" ? "max-w-[24rem]" : "max-w-[52rem]"} p-[40px]`}
|
||||
>
|
||||
<ModalHeader className="flex flex-col p-0">
|
||||
{title && <h3>{title}</h3>}
|
||||
{subtitle && (
|
||||
<span className="text-neutral-400 text-sm font-light">
|
||||
{subtitle}
|
||||
</span>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalBody className="px-0 py-[20px]">{children}</ModalBody>
|
||||
{(primaryAction || secondaryAction) && (
|
||||
<ModalFooter
|
||||
className={`${size === "sm" ? "flex-col" : "flex-row"} flex justify-start p-0`}
|
||||
>
|
||||
{primaryAction}
|
||||
{secondaryAction}
|
||||
</ModalFooter>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ODModal.defaultProps = {
|
||||
title: "",
|
||||
subtitle: "",
|
||||
primaryAction: null,
|
||||
secondaryAction: null,
|
||||
};
|
||||
|
||||
export default ODModal;
|
||||
@ -4,11 +4,6 @@ import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
Button,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Select,
|
||||
SelectItem,
|
||||
} from "@nextui-org/react";
|
||||
@ -25,6 +20,7 @@ import { RootState } from "../store";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import { AvailableLanguages } from "../i18n";
|
||||
import { ArgConfigType } from "../types/ConfigType";
|
||||
import ODModal from "./ODModal";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
@ -88,82 +84,83 @@ function InnerSettingModal({ isOpen, onClose }: Props): JSX.Element {
|
||||
item.toLowerCase().includes(input.toLowerCase());
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} hideCloseButton backdrop="blur">
|
||||
<ModalContent>
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
{t(I18nKey.CONFIGURATION$MODAL_TITLE)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Autocomplete
|
||||
defaultItems={supportedModels.map((v: string) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}))}
|
||||
label={t(I18nKey.CONFIGURATION$MODEL_SELECT_LABEL)}
|
||||
placeholder={t(I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER)}
|
||||
selectedKey={model}
|
||||
onSelectionChange={(key) => {
|
||||
setModel(key as string);
|
||||
}}
|
||||
onInputChange={(e) => setInputModel(e)}
|
||||
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
|
||||
defaultFilter={customFilter}
|
||||
defaultInputValue={inputModel}
|
||||
allowsCustomValue
|
||||
>
|
||||
{(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={t(I18nKey.CONFIGURATION$AGENT_SELECT_LABEL)}
|
||||
placeholder={t(I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER)}
|
||||
defaultSelectedKey={agent}
|
||||
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>
|
||||
<Select
|
||||
selectionMode="single"
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
selectedKeys={[language]}
|
||||
label={t(I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL)}
|
||||
>
|
||||
{AvailableLanguages.map((lang) => (
|
||||
<SelectItem key={lang.value} value={lang.value}>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button color="danger" variant="light" onPress={onClose}>
|
||||
{t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL)}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSaveCfg}>
|
||||
{t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL)}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ODModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t(I18nKey.CONFIGURATION$MODAL_TITLE)}
|
||||
subtitle={t(I18nKey.CONFIGURATION$MODAL_SUB_TITLE)}
|
||||
hideCloseButton
|
||||
backdrop="blur"
|
||||
size="sm"
|
||||
primaryAction={
|
||||
<Button className="bg-primary rounded-small" onPress={handleSaveCfg}>
|
||||
{t(I18nKey.CONFIGURATION$MODAL_SAVE_BUTTON_LABEL)}
|
||||
</Button>
|
||||
}
|
||||
secondaryAction={
|
||||
<Button className="bg-neutral-500 rounded-small" onPress={onClose}>
|
||||
{t(I18nKey.CONFIGURATION$MODAL_CLOSE_BUTTON_LABEL)}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Autocomplete
|
||||
defaultItems={supportedModels.map((v: string) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
}))}
|
||||
label={t(I18nKey.CONFIGURATION$MODEL_SELECT_LABEL)}
|
||||
placeholder={t(I18nKey.CONFIGURATION$MODEL_SELECT_PLACEHOLDER)}
|
||||
selectedKey={model}
|
||||
onSelectionChange={(key) => {
|
||||
setModel(key as string);
|
||||
}}
|
||||
onInputChange={(e) => setInputModel(e)}
|
||||
onKeyDown={(e: KeyboardEvent) => e.continuePropagation()}
|
||||
defaultFilter={customFilter}
|
||||
defaultInputValue={inputModel}
|
||||
allowsCustomValue
|
||||
>
|
||||
{(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={t(I18nKey.CONFIGURATION$AGENT_SELECT_LABEL)}
|
||||
placeholder={t(I18nKey.CONFIGURATION$AGENT_SELECT_PLACEHOLDER)}
|
||||
defaultSelectedKey={agent}
|
||||
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>
|
||||
<Select
|
||||
selectionMode="single"
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
selectedKeys={[language]}
|
||||
label={t(I18nKey.CONFIGURATION$LANGUAGE_SELECT_LABEL)}
|
||||
>
|
||||
{AvailableLanguages.map((lang) => (
|
||||
<SelectItem key={lang.value} value={lang.value}>
|
||||
{lang.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</>
|
||||
</ODModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -95,6 +95,18 @@
|
||||
"es": "Configuración",
|
||||
"tr": "Konfigürasyon"
|
||||
},
|
||||
"CONFIGURATION$MODAL_SUB_TITLE": {
|
||||
"en": "Adjust settings to your liking",
|
||||
"zh-CN": "根据您的喜好调整设置",
|
||||
"de": "Passen Sie die Einstellungen nach Ihren Wünschen an ",
|
||||
"ko-KR": "원하는 대로 설정 조정",
|
||||
"no": "Juster innstillinger etter dine ønsker ",
|
||||
"zh-TW": "調整設定以符合您的喜好",
|
||||
"it": "Regola le impostazioni in base alle tue preferenze",
|
||||
"pt": "Ajuste as configurações de acordo com sua preferência",
|
||||
"es": "Ajusta la configuración a tu gusto",
|
||||
"tr": "Ayarları isteğinize göre ayarlayın"
|
||||
},
|
||||
"CONFIGURATION$MODEL_SELECT_LABEL": {
|
||||
"en": "Model",
|
||||
"zh-CN": "模型",
|
||||
@ -201,7 +213,7 @@
|
||||
"it": "Invia un messaggio (non interromperà l'Assistente)",
|
||||
"pt": "Envie uma mensagem (não interromperá o Assistente)",
|
||||
"es": "Enviar un mensaje (no interrumpirá al Asistente)",
|
||||
"tr": "Bir mesaj gönderin (Asistan Kesilmeyecek)"
|
||||
"tr": "Bir mesaj gönderin (Asistan Kesilmeyecek)"
|
||||
},
|
||||
"CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
|
||||
"en": "Send",
|
||||
@ -215,4 +227,4 @@
|
||||
"es": "Enviar",
|
||||
"tr": "Gönder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,27 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const { nextui } = require("@nextui-org/react");
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
nextui({
|
||||
defaultTheme: "dark",
|
||||
}),
|
||||
],
|
||||
content: [
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
plugins: [
|
||||
nextui({
|
||||
defaultTheme: "dark",
|
||||
layout: {
|
||||
radius: {
|
||||
small: "5px",
|
||||
large: "20px",
|
||||
},
|
||||
},
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
primary:"#4465DB",
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user