diff --git a/agenta-web/cypress/support/commands/evaluations.ts b/agenta-web/cypress/support/commands/evaluations.ts index 9fd645d451..c5420b310c 100644 --- a/agenta-web/cypress/support/commands/evaluations.ts +++ b/agenta-web/cypress/support/commands/evaluations.ts @@ -24,26 +24,18 @@ Cypress.Commands.add("createVariant", () => { cy.get('[data-cy="create-new-app-button"]').click() cy.get('[data-cy="create-from-template"]').click() } else { - cy.get('[data-cy="create-from-template__no-app"]').click() + cy.get('[data-cy="create-from-template"]').click() } }) - cy.contains("Single Prompt OpenAI") - .parentsUntil('[data-cy^="app-template-card"]') - .last() - .contains("create app", {matchCase: false}) - .click() - const appName = randString(5) cy.task("log", `App name: ${appName}`) - cy.get('[data-cy="enter-app-name-modal"]') - .should("exist") - .within(() => { - cy.get("input").type(appName) - }) + cy.get('[data-cy^="enter-app-name-input"]').type(appName) + + cy.get('[data-cy="app-template-card"]').contains("Single Prompt OpenAI").click() - cy.get('[data-cy="enter-app-name-modal-button"]').click() + cy.get('[data-cy="create-app-from-template-button"]').click() cy.url().should("include", "/playground") cy.url().then((url) => { diff --git a/agenta-web/public/assets/dark-complete-transparent_white_logo.png b/agenta-web/public/assets/dark-complete-transparent_white_logo.png new file mode 100644 index 0000000000..8685bbf981 Binary files /dev/null and b/agenta-web/public/assets/dark-complete-transparent_white_logo.png differ diff --git a/agenta-web/src/components/AppSelector/AppCard.tsx b/agenta-web/src/components/AppSelector/AppCard.tsx index 64f2a95f85..d0e6fa501f 100644 --- a/agenta-web/src/components/AppSelector/AppCard.tsx +++ b/agenta-web/src/components/AppSelector/AppCard.tsx @@ -16,15 +16,14 @@ const {Text} = Typography const useStyles = createUseStyles((theme: JSSTheme) => ({ card: { - width: 300, display: "flex", flexDirection: "column", transition: "all 0.025s ease-in", cursor: "pointer", + boxShadow: theme.boxShadowTertiary, "& > .ant-card-head": { minHeight: 0, - padding: theme.paddingSM, - + padding: `${theme.paddingXS}px ${theme.paddingSM}px`, "& .ant-card-head-title": { fontSize: theme.fontSizeLG, fontWeight: theme.fontWeightMedium, @@ -88,7 +87,7 @@ const AppCard: React.FC<{ onClick={() => router.push(`/apps/${app.app_id}/overview`)} extra={ ({ container: ({themeMode}: StyleProps) => ({ @@ -28,41 +30,22 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ width: "100%", color: themeMode === "dark" ? "#fff" : "#000", }), - cardsList: ({themeMode}: StyleProps) => ({ - display: "flex", - flexWrap: "wrap", + cardsList: { + width: "100%", + display: "grid", gap: 16, - "& .ant-card-bordered, .ant-card-actions": { - borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.1)", + "@media (max-width: 1199px)": { + gridTemplateColumns: "repeat(2, minmax(0, 1fr))", }, - }), - createCard: ({themeMode}: StyleProps) => ({ - fontSize: 20, - backgroundColor: themeMode === "dark" ? "" : "hsl(0, 0%, 100%)", - borderColor: themeMode === "dark" ? "hsl(0, 0%, 100%)" : "hsl(0, 0%, 10%) !important", - color: themeMode === "dark" ? "#fff" : "#000", - boxShadow: "0px 4px 8px rgba(0, 0, 0, 0.1)", - - width: 300, - height: 120, - display: "flex", - alignItems: "center", - justifyContent: "center", - cursor: "pointer", - "& .ant-card-meta-title": { - color: themeMode === "dark" ? "#fff" : "#000", + "@media (min-width: 1200px) and (max-width: 1699px)": { + gridTemplateColumns: "repeat(3, minmax(0, 1fr))", + }, + "@media (min-width: 1700px) and (max-width: 2000px)": { + gridTemplateColumns: "repeat(4, minmax(0, 1fr))", + }, + "@media (min-width: 2001px)": { + gridTemplateColumns: "repeat(5, minmax(0, 1fr))", }, - }), - createCardMeta: ({themeMode}: StyleProps) => ({ - height: "90%", - display: "flex", - alignItems: "center", - justifyContent: "space-evenly", - color: themeMode === "dark" ? "#fff" : "#000", - }), - closeIcon: { - fontSize: 20, - color: "red", }, title: { fontSize: 16, @@ -70,19 +53,24 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ lineHeight: "24px", }, modal: { - "& .ant-modal-body": { - display: "flex", - flexDirection: "column", - gap: "0.75rem", - marginTop: 20, + transition: "width 0.3s ease", + "& .ant-modal-content": { + overflow: "hidden", + borderRadius: 16, + "& > .ant-modal-close": { + top: 16, + }, }, }, - modalError: { - color: "red", - marginLeft: "10px", + appTemplate: { + gap: 16, + display: "flex", + flexDirection: "column", }, - modalBtn: { - alignSelf: "flex-end", + headerText: { + lineHeight: theme.lineHeightLG, + fontSize: theme.fontSizeHeading4, + fontWeight: theme.fontWeightStrong, }, })) @@ -93,17 +81,16 @@ const AppSelector: React.FC = () => { const {appTheme} = useAppTheme() const classes = useStyles({themeMode: appTheme} as StyleProps) const [isCreateAppModalOpen, setIsCreateAppModalOpen] = useState(false) - const [isCreateAppFromTemplateModalOpen, setIsCreateAppFromTemplateModalOpen] = useState(false) - const [isWriteAppModalOpen, setIsWriteAppModalOpen] = useState(false) const [isMaxAppModalOpen, setIsMaxAppModalOpen] = useState(false) const [templates, setTemplates] = useState([]) const {user} = useProfileData() - const [templateMessage, setTemplateMessage] = useState("") + const [noTemplateMessage, setNoTemplateMessage] = useState("") const [templateId, setTemplateId] = useState(undefined) - const [isInputTemplateModalOpen, setIsInputTemplateModalOpen] = useState(false) const [statusModalOpen, setStatusModalOpen] = useState(false) const [fetchingTemplate, setFetchingTemplate] = useState(false) const [newApp, setNewApp] = useState("") + const [current, setCurrent] = useState(0) + const [searchTerm, setSearchTerm] = useState("") const {apps, error, isLoading, mutate} = useAppsData() const [statusData, setStatusData] = useState<{status: string; details?: any; appId?: string}>({ status: "", @@ -113,6 +100,8 @@ const AppSelector: React.FC = () => { const [useOrgData, setUseOrgData] = useState(() => () => "") const {selectedOrg} = useOrgData() + const hasAvailableApps = Array.isArray(apps) && apps.length > 0 + useEffect(() => { dynamicContext("org.context", {useOrgData}).then((context) => { setUseOrgData(() => context.useOrgData) @@ -126,38 +115,17 @@ const AppSelector: React.FC = () => { const showMaxAppError = () => { setIsMaxAppModalOpen(true) } + const showCreateAppFromTemplateModal = () => { setTemplateId(undefined) setNewApp("") - setIsCreateAppModalOpen(false) - setIsCreateAppFromTemplateModalOpen(true) + setIsCreateAppModalOpen(true) + setCurrent(hasAvailableApps ? 1 : 0) } const showWriteAppModal = () => { - setIsCreateAppModalOpen(false) - setIsWriteAppModalOpen(true) - } - - const showInputTemplateModal = () => { - setIsCreateAppFromTemplateModalOpen(false) - setIsInputTemplateModalOpen(true) - } - - const handleCreateAppFromTemplateModalCancel = () => { - setIsCreateAppFromTemplateModalOpen(false) - } - - const handleWriteApppModalCancel = () => { - setIsWriteAppModalOpen(false) - } - - const handleCreateAppModalCancel = () => { - setIsCreateAppModalOpen(false) - } - - const handleInputTemplateModalCancel = () => { - if (fetchingTemplate) return - setIsInputTemplateModalOpen(false) + setIsCreateAppModalOpen(true) + setCurrent(hasAvailableApps ? 2 : 1) } useEffect(() => { @@ -167,7 +135,7 @@ const AppSelector: React.FC = () => { if (typeof data == "object") { setTemplates(data) } else { - setTemplateMessage(data) + setNoTemplateMessage(data) } } @@ -175,10 +143,7 @@ const AppSelector: React.FC = () => { }, []) const handleTemplateCardClick = async (template_id: string) => { - handleInputTemplateModalCancel() - handleCreateAppFromTemplateModalCancel() - handleCreateAppModalCancel() - + setIsCreateAppModalOpen(false) // warn the user and redirect if openAI key is not present // TODO: must be changed for multiples LLM keys if (redirectIfNoLLMKeys()) return @@ -242,12 +207,6 @@ const AppSelector: React.FC = () => { [apps, newApp], ) - const handleEnterKeyPress = (event: React.KeyboardEvent) => { - if (event.key === "Enter") { - handleCreateApp() - } - } - const handleCreateApp = () => { if (appNameExist) { notification.warning({ @@ -272,6 +231,60 @@ const AppSelector: React.FC = () => { } } + const filteredApps = useMemo(() => { + let filtered = apps.sort( + (a, b) => dayjs(b.updated_at).valueOf() - dayjs(a.updated_at).valueOf(), + ) + + if (searchTerm) { + filtered = apps.filter((app) => + app.app_name.toLowerCase().includes(searchTerm.toLowerCase()), + ) + } + return filtered + }, [apps, searchTerm]) + + const steps = [ + { + content: ( + { + setTemplateId(template.id) + }} + handleCreateApp={handleCreateApp} + /> + ), + }, + { + content: ( + + ), + }, + ] + + if (hasAvailableApps) { + steps.unshift({ + content: ( +
+ Add new app + + +
+ ), + }) + } + return ( { )} - {isLoading ? ( + {isLoading || (!apps && !error) ? (
@@ -314,19 +327,29 @@ const AppSelector: React.FC = () => { ) : Array.isArray(apps) && apps.length ? ( -
-
- {Array.isArray(apps) && ( - <> - {apps.map((app, index: number) => ( -
- -
- ))} - - )} +
+
+ setSearchTerm(e.target.value)} + /> +
+ {Array.isArray(apps) && filteredApps.length ? ( +
+ {filteredApps.map((app, index: number) => ( +
+ +
+ ))} +
+ ) : ( + + )} +
) : ( @@ -337,63 +360,28 @@ const AppSelector: React.FC = () => { )}
- - { - showInputTemplateModal() - setTemplateId(template.id) + afterClose={() => setCurrent(0)} + onCancel={() => { + setIsCreateAppModalOpen(false) }} - /> + footer={null} + title={null} + className={classes.modal} + width={steps.length === 3 && current == 0 ? 845 : 480} + centered + > + {steps[current]?.content} + + { setIsMaxAppModalOpen(false) }} /> - - setNewApp(e.target.value)} - onKeyDown={handleEnterKeyPress} - disabled={fetchingTemplate} - /> - {appNameExist &&
App name already exists
} - {newApp.length > 0 && !isAppNameInputValid(newApp) && ( -
- App name must contain only letters, numbers, underscore, or dash -
- )} - -
+ { statusData={statusData} appName={newApp} /> - - ) } diff --git a/agenta-web/src/components/AppSelector/AppTemplateCard.tsx b/agenta-web/src/components/AppSelector/AppTemplateCard.tsx index c7c4ff8984..864cc1ef7f 100644 --- a/agenta-web/src/components/AppSelector/AppTemplateCard.tsx +++ b/agenta-web/src/components/AppSelector/AppTemplateCard.tsx @@ -1,79 +1,127 @@ -import {Button, Card, Tag, Typography} from "antd" +import React from "react" import {createUseStyles} from "react-jss" +import {JSSTheme} from "@/lib/Types" +import {Button, Card, Typography} from "antd" +import {ArrowRight} from "@phosphor-icons/react" -type StylesProp = { - tag: string | undefined -} - -const {Text} = Typography - -const useStyles = createUseStyles({ +const useStyles = createUseStyles((theme: JSSTheme) => ({ card: { - "& .ant-card-body": { - padding: "1rem", - display: "flex", - alignItems: "center", - justifyContent: "space-evenly", - flexDirection: "column", - position: "relative", + width: 392, + height: 268, + display: "flex", + cursor: "pointer", + flexDirection: "column", + justifyContent: "space-between", + transition: "all 0.025s ease-in", + boxShadow: theme.boxShadowTertiary, + "& > .ant-card-head": { + minHeight: 0, + padding: theme.paddingSM, + "& .ant-card-head-title": { + fontSize: theme.fontSizeLG, + fontWeight: theme.fontWeightMedium, + }, + }, + "& > .ant-card-body": { + padding: theme.paddingSM, + flex: 1, + }, + "& > .ant-card-actions": { + padding: "0 12px", + }, + "&:hover": { + boxShadow: theme.boxShadow, }, }, - tag: { - position: "absolute", - right: 0, - top: 8, - }, - text1: ({tag}: StylesProp) => ({ - marginBottom: -4, - marginTop: tag ? 20 : 0, - fontSize: 15, - }), - link: { - textAlign: "center", - }, - createBtn: { + button: { width: "100%", + display: "flex", + alignItems: "center", + "& > .ant-btn-icon": { + marginTop: 4, + }, }, -}) +})) interface Props { - title: string - onClick: () => void - body: string - noTemplate: boolean - tag?: string + onWriteOwnApp: () => void + onCreateFromTemplate: () => void } -const AppTemplateCard: React.FC = ({title, tag, onClick, body, noTemplate}) => { - const classes = useStyles({tag} as StylesProp) +const AppTemplateCard: React.FC = ({onWriteOwnApp, onCreateFromTemplate}) => { + const classes = useStyles() + + const templatePoints = [ + "Experiment and compare prompts and models", + "Evaluate outputs in the web UI", + "Deploy and version prompts", + "Track all LLM calls", + ] + const complexLLM = [ + "Experiment with RAG, or agent architectures in the web UI", + "Create custom playgrounds to debug and trace calls", + "Easily integrate your LLM app code into the platform", + "Evaluate workflows end-to-end in the web UI", + ] return ( - - {tag && ( - - {tag} - - )} - - {title} - +
+ } + size="large" + > + Create a new prompt + , + ]} + > +
+ Quickly create a prompt and: +
    + {templatePoints.map((item) => ( +
  • {item}
  • + ))} +
+
+
- {noTemplate ? ( - -

- {body} here. -

-
- ) : ( -
- -

{body}

-
- + } + size="large" + > + Create your own app + , + ]} + > +
+ + Create your own complex application using any framework. + +
    + {complexLLM.map((item) => ( +
  • {item}
  • + ))} +
- )} -
+ +
) } diff --git a/agenta-web/src/components/AppSelector/Welcome.tsx b/agenta-web/src/components/AppSelector/Welcome.tsx index 9620bc4b53..aaf12f0c26 100644 --- a/agenta-web/src/components/AppSelector/Welcome.tsx +++ b/agenta-web/src/components/AppSelector/Welcome.tsx @@ -1,50 +1,7 @@ import React from "react" -import {createUseStyles} from "react-jss" -import {JSSTheme} from "@/lib/Types" import Image from "next/image" -import {Button, Card, Typography} from "antd" -import {ArrowRight} from "@phosphor-icons/react" - -const useStyles = createUseStyles((theme: JSSTheme) => ({ - card: { - width: 392, - height: 268, - display: "flex", - cursor: "pointer", - flexDirection: "column", - justifyContent: "space-between", - transition: "all 0.025s ease-in", - boxShadow: - "0px 2px 4px 0px rgba(0, 0, 0, 0.02), 0px 1px 6px -1px rgba(0, 0, 0, 0.02), 0px 1px 2px 0px rgba(0, 0, 0, 0.03)", - "& > .ant-card-head": { - minHeight: 0, - padding: theme.paddingSM, - - "& .ant-card-head-title": { - fontSize: theme.fontSizeLG, - fontWeight: theme.fontWeightMedium, - }, - }, - "& > .ant-card-body": { - padding: theme.paddingSM, - flex: 1, - }, - "& > .ant-card-actions": { - padding: "0 12px", - }, - "&:hover": { - boxShadow: theme.boxShadow, - }, - }, - button: { - width: "100%", - display: "flex", - alignItems: "center", - "& > .ant-btn-icon": { - marginTop: 4, - }, - }, -})) +import {Typography} from "antd" +import AppTemplateCard from "./AppTemplateCard" interface Props { onWriteOwnApp: () => void @@ -52,21 +9,6 @@ interface Props { } const Welcome: React.FC = ({onWriteOwnApp, onCreateFromTemplate}) => { - const classes = useStyles() - - const templatePoints = [ - "Compare prompts and models", - "Create testsets", - "Evaluate outputs", - "Deploy in one click", - ] - const complexLLM = [ - "Use Langchain, Llama Index, or any framework", - "Use OpenAI, Cohere, or self-hosted open-source models", - "Continue in the UI: Everything in the left", - "Streamline collaboration between devs and domain experts!", - ] - return (
@@ -83,64 +25,10 @@ const Welcome: React.FC = ({onWriteOwnApp, onCreateFromTemplate}) => {
- } - size="large" - > - Start with a template - , - ]} - > -
- - Setup an app using our preset LLM config and explore Agenta AI - -
    - {templatePoints.map((item) => ( -
  • {item}
  • - ))} -
-
-
- - } - size="large" - > - Setup your own app - , - ]} - > -
- - Create your own complex application using any framework. - -
    - {complexLLM.map((item) => ( -
  • {item}
  • - ))} -
-
-
+
) diff --git a/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx b/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx index 2a7e3069fb..6125448b11 100644 --- a/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/AddAppFromTemplateModal.tsx @@ -1,90 +1,185 @@ -import {Modal, Typography} from "antd" -import React from "react" +import {Typography, Input, Card, Radio, Space, Button, Flex} from "antd" +import {ArrowLeft} from "@phosphor-icons/react" import {createUseStyles} from "react-jss" -import AppTemplateCard from "../AppTemplateCard" -import {Template} from "@/lib/Types" +import {JSSTheme, Template} from "@/lib/Types" +import {isAppNameInputValid} from "@/lib/helpers/utils" -const useStyles = createUseStyles({ +const {Text} = Typography + +const useStyles = createUseStyles((theme: JSSTheme) => ({ modal: { - "& .ant-modal-close": { - top: 23, + display: "flex", + flexDirection: "column", + gap: 24, + }, + modalError: { + color: theme.colorError, + marginTop: 2, + }, + headerText: { + "& .ant-typography": { + lineHeight: theme.lineHeightLG, + fontSize: theme.fontSizeHeading4, + fontWeight: theme.fontWeightStrong, }, }, title: { - margin: 0, + fontSize: theme.fontSizeLG, + fontWeight: theme.fontWeightMedium, + lineHeight: theme.lineHeightLG, }, - body: { - width: "100%", - marginTop: 20, - display: "flex", - gap: 20, - }, - row: { - marginTop: 20, + label: { + fontWeight: theme.fontWeightMedium, }, -}) + card: { + width: 208, + height: 180, + cursor: "pointer", + transitionDuration: "0.3s", + "&:hover": { + boxShadow: theme.boxShadow, + }, + "& > .ant-card-head": { + minHeight: 0, + padding: theme.paddingSM, -const {Title} = Typography + "& .ant-card-head-title": { + fontSize: theme.fontSize, + fontWeight: theme.fontWeightMedium, + lineHeight: theme.lineHeight, + }, + }, + "& > .ant-card-body": { + padding: theme.paddingSM, + "& > .ant-typography": { + color: theme.colorTextSecondary, + }, + }, + }, + inputName: { + borderColor: `${theme.colorError} !important`, + "& .ant-input-clear-icon": { + color: theme.colorError, + }, + }, +})) -type Props = React.ComponentProps & { +type Props = { + setCurrent: React.Dispatch> + hasAvailableApps: boolean newApp: string + setNewApp: React.Dispatch> templates: Template[] noTemplateMessage: string onCardClick: (template: Template) => void + appNameExist: boolean + handleCreateApp: () => void + templateId: string | undefined } -const AddAppFromTemplatedModal: React.FC = ({ +const AddAppFromTemplatedModal = ({ + setCurrent, + hasAvailableApps, newApp, + setNewApp, templates, noTemplateMessage, onCardClick, - ...props -}) => { + appNameExist, + handleCreateApp, + templateId, +}: Props) => { const classes = useStyles() + const isError = appNameExist || (newApp.length > 0 && !isAppNameInputValid(newApp)) + + const handleEnterKeyPress = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && templateId) { + handleCreateApp() + } + } + return ( - - Choose template - - } - width={templates.length <= 1 || !!noTemplateMessage ? 620 : 700} - {...props} - > -
- {noTemplateMessage ? ( -
- {}} - /> -
- ) : ( - templates.map((template) => ( -
- + + {hasAvailableApps && ( +
- )) - )} +
+
- + ) } diff --git a/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx b/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx deleted file mode 100644 index c458a758be..0000000000 --- a/agenta-web/src/components/AppSelector/modals/AddNewAppModal.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import {useAppTheme} from "@/components/Layout/ThemeContextProvider" -import {AppstoreAddOutlined, CodeOutlined} from "@ant-design/icons" -import {Col, Modal, Row, Typography} from "antd" -import React from "react" -import {createUseStyles} from "react-jss" -import {StyleProps} from "@/lib/Types" - -const useStyles = createUseStyles({ - modal: { - "& .ant-modal-close": { - top: 23, - }, - }, - title: { - margin: 0, - }, - row: { - marginTop: 20, - }, - col: ({themeMode}: StyleProps) => ({ - display: "flex", - flexDirection: "column", - alignItems: "center", - textAlign: "center", - justifyContent: "center", - padding: "1rem", - cursor: "pointer", - border: `1px solid`, - borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.15)", - borderRadius: 8, - transition: "all 0.3s", - - "& > .anticon": { - fontSize: 24, - }, - "& > h5": { - marginTop: "0.75rem", - marginBottom: "0.25rem", - color: "inherit", - }, - "& > .ant-typography-secondary": { - fontSize: 13, - }, - "&:hover": { - borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.4)" : "rgba(5, 5, 5, 0.3)", - }, - }), -}) - -const {Title, Text} = Typography - -type Props = React.ComponentProps & { - onCreateFromTemplate: () => void - onWriteOwnApp: () => void -} - -const AddNewAppModal: React.FC = ({onCreateFromTemplate, onWriteOwnApp, ...props}) => { - const {appTheme} = useAppTheme() - const classes = useStyles({themeMode: appTheme} as StyleProps) - - return ( - - Add new app - - } - width={600} - {...props} - > - - -
- - Create From Template - Create Quickly Simple Prompt Apps From UI -
- - -
- - Write Your Own App - Create Complex LLM Apps From Your Code -
- -
-
- ) -} - -export default AddNewAppModal diff --git a/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx b/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx index 3a7d655407..516ac87db0 100644 --- a/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/CreateAppStatusModal.tsx @@ -1,30 +1,56 @@ -import {GenericObject} from "@/lib/Types" +import {GenericObject, JSSTheme} from "@/lib/Types" import {getErrorMessage} from "@/lib/helpers/errorHandler" -import {CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined} from "@ant-design/icons" -import {Alert, Modal, Typography, theme} from "antd" +import {Modal, Typography, theme} from "antd" import {useRouter} from "next/router" import React, {useEffect, useState} from "react" import {createUseStyles} from "react-jss" +import {Check, CircleNotch, ExclamationMark} from "@phosphor-icons/react" +import CustomAppCreationLoader from "./CustomAppCreationLoader" -const useStyles = createUseStyles({ +const {Text} = Typography + +const useStyles = createUseStyles((theme: JSSTheme) => ({ statusRow: { - marginTop: 8, display: "flex", - alignItems: "flex-start", + alignItems: "center", gap: 8, - - "& .anticon": { - marginTop: 4, + }, + modal: { + "& .ant-modal-content": { + padding: 0, + overflow: "hidden", + borderRadius: 16, + "& > .ant-modal-footer": { + padding: theme.paddingContentHorizontalLG, + paddingTop: 0, + }, }, }, - warning: { - margin: "8px 0", - marginLeft: -2, + topContainer: { + wdith: "100%", + height: 200, + backgroundColor: "#F5F7FA", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + bottomContainer: { + padding: theme.paddingContentHorizontalLG, + display: "grid", + gap: 10, + }, + headerText: { + lineHeight: theme.lineHeightLG, + fontSize: theme.fontSizeHeading4, + fontWeight: theme.fontWeightStrong, + }, + error: { + color: theme.colorError, }, - statusSteps: { - marginTop: 12, + subText: { + color: theme.colorTextSecondary, }, -}) +})) interface Props { loading: boolean @@ -45,7 +71,7 @@ const CreateAppStatusModal: React.FC> const router = useRouter() const classes = useStyles() const { - token: {colorError, colorSuccess, colorPrimary}, + token: {colorError, cyan5: colorSuccess}, } = theme.useToken() const [messages, setMessages] = useState<{ [status: string]: { @@ -54,7 +80,6 @@ const CreateAppStatusModal: React.FC> errorMessage?: string } }>({}) - const [isDelayed, setIsDelayed] = useState(false) const {appId, status, details} = statusData const isError = ["bad_request", "error"].includes(status) @@ -64,7 +89,6 @@ const CreateAppStatusModal: React.FC> const reset = () => { setMessages({}) - setIsDelayed(false) } const onOk = (e: any) => { @@ -88,7 +112,7 @@ const CreateAppStatusModal: React.FC> ...prev, [status]: { type: "loading", - message: "Creating variant from template image", + message: "Adding application", }, } if (obj.fetching_image?.type === "loading") obj.fetching_image.type = "success" @@ -98,7 +122,7 @@ const CreateAppStatusModal: React.FC> ...prev, [status]: { type: "loading", - message: "Waiting for the app to start", + message: "Starting service (takes ~20s)", }, } if (obj.creating_app?.type === "loading") obj.creating_app.type = "success" @@ -108,7 +132,7 @@ const CreateAppStatusModal: React.FC> ...prev, [status]: { type: "success", - message: "App created successfully!", + message: "Launching your application", }, } if (obj.starting_app?.type === "loading") obj.starting_app.type = "success" @@ -147,16 +171,6 @@ const CreateAppStatusModal: React.FC> }) }, [status]) - useEffect(() => { - if (!props.open) reset() - else { - const timeout = setTimeout(() => { - setIsDelayed(true) - }, 20000) - return () => clearTimeout(timeout) - } - }, [props.open]) - return ( > okText={"Retry"} footer={closable ? undefined : null} closable={closable} - title="App Creation Status" + title={null} {...props} onCancel={closable ? props.onCancel : undefined} + className={classes.modal} + width={480} + centered > - {!closable && isDelayed && ( - - )} - - Creating your app "{appName}". This takes around 30 seconds. - - -
- {Object.values(messages).map(({type, message, errorMessage}, ix) => ( -
- {type === "success" ? ( - - ) : type === "error" ? ( - +
+
+
+ {closable ? ( +
+ + Oops, something went wrong. + + {isError && getErrorMessage(details)}{" "} + {isTimeout && + 'The app took too long to start. Press the "Retry" button if you want to try again.'} + +
) : ( - + )} - - {message} - {errorMessage && ( - <> -
- {errorMessage} - - )} -
- ))} -
+
+ +
+ Creating your new app + {Object.values(messages).map(({type, message}) => ( +
+ {type === "success" ? ( + + ) : type === "error" ? ( + + ) : ( + + )} + + {message}{" "} + {message == "Adding application" && ( + {appName} + )} + +
+ ))} +
+ ) } diff --git a/agenta-web/src/components/AppSelector/modals/CustomAppCreationLoader.tsx b/agenta-web/src/components/AppSelector/modals/CustomAppCreationLoader.tsx new file mode 100644 index 0000000000..fac1c43853 --- /dev/null +++ b/agenta-web/src/components/AppSelector/modals/CustomAppCreationLoader.tsx @@ -0,0 +1,245 @@ +import React from "react" + +const CustomAppCreationLoader = ({isFinish}: {isFinish: boolean}) => { + return ( +
+ + + + + + + + + + + + + + + + + + + + {isFinish && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + +
+ ) +} + +export default CustomAppCreationLoader diff --git a/agenta-web/src/components/AppSelector/modals/EditAppModal.tsx b/agenta-web/src/components/AppSelector/modals/EditAppModal.tsx index a55e883282..5358a232be 100644 --- a/agenta-web/src/components/AppSelector/modals/EditAppModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/EditAppModal.tsx @@ -6,6 +6,7 @@ import {CheckOutlined} from "@ant-design/icons" import {Input, Modal, Typography} from "antd" import React, {useMemo, useState} from "react" import {createUseStyles} from "react-jss" +import {useUpdateEffect} from "usehooks-ts" type EditAppModalProps = { appDetails: ListAppsItem @@ -29,6 +30,10 @@ const EditAppModal = ({appDetails, ...props}: EditAppModalProps) => { const [appNameInput, setAppNameInput] = useState(appDetails.app_name) const [editAppLoading, setEditAppLoading] = useState(false) + useUpdateEffect(() => { + setAppNameInput(appDetails.app_name) + }, [apps]) + const appNameExist = useMemo( () => apps.some( diff --git a/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx b/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx index 1885a43f16..789cd049ae 100644 --- a/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/MaxAppModal.tsx @@ -1,86 +1,85 @@ -import {useAppTheme} from "@/components/Layout/ThemeContextProvider" -import {AppstoreAddOutlined, CodeOutlined} from "@ant-design/icons" -import {Col, Modal, Row, Typography} from "antd" +import Image from "next/image" +import {Button, Modal, Typography} from "antd" import React from "react" import {createUseStyles} from "react-jss" -import {StyleProps} from "@/lib/Types" +import {JSSTheme} from "@/lib/Types" +import {Phone, SlackLogo} from "@phosphor-icons/react" +import Link from "next/link" -const useStyles = createUseStyles({ +const useStyles = createUseStyles((theme: JSSTheme) => ({ modal: { + "& .ant-modal-content": { + borderRadius: 16, + }, "& .ant-modal-close": { - top: 23, + top: 17, }, }, - title: { - margin: 0, - }, - row: { - marginTop: 20, - }, - col: ({themeMode}: StyleProps) => ({ + image: { + background: "linear-gradient(180deg, #1F2D43 0%, #3A547E 100%)", + borderRadius: theme.borderRadiusLG, + width: "100%", + height: 140, display: "flex", - flexDirection: "column", alignItems: "center", - textAlign: "center", justifyContent: "center", - padding: "1rem", - cursor: "pointer", - border: `1px solid`, - borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.2)" : "rgba(5, 5, 5, 0.15)", - borderRadius: 8, - transition: "all 0.3s", - - "& > .anticon": { - fontSize: 24, - }, - "& > h5": { - marginTop: "0.75rem", - marginBottom: "0.25rem", - color: "inherit", - }, - "& > .ant-typography-secondary": { - fontSize: 13, - }, - "&:hover": { - borderColor: themeMode === "dark" ? "rgba(256, 256, 256, 0.4)" : "rgba(5, 5, 5, 0.3)", - }, - }), -}) - -const {Title, Text} = Typography + }, +})) type Props = React.ComponentProps & {} const MaxAppModal: React.FC = ({...props}) => { - const {appTheme} = useAppTheme() - const classes = useStyles({themeMode: appTheme} as StyleProps) + const classes = useStyles() return ( - Free Limit Reached - - } - width={600} + title="Unlock unlimited applications" + width={480} {...props} > -
You've reached the maximum number of apps for the free version.
-
- To create more apps{" "} - schedule a call to get - full access to the platform. -
-
- Questions? Feel free to reach out to our support in{" "} - - Slack - - . -
+
+
+ aenta-ai +
+
+ + Hey, it seems like you have reached your free limit.{" "} + + + To create more applications, please schedule a call to get full access to + the platform. + + + Got any questions? Feel free to reach out to our support in Slack. + +
+ +
+ + + + + + + + +
+
) } diff --git a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx index c00d7c1829..48fca7bcf4 100644 --- a/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx +++ b/agenta-web/src/components/AppSelector/modals/WriteOwnAppModal.tsx @@ -1,209 +1,160 @@ -import Link from "next/link" -import CopyButton from "@/components/CopyButton/CopyButton" -import {useAppTheme} from "@/components/Layout/ThemeContextProvider" -import {Modal, Typography} from "antd" -import React, {useEffect, useRef} from "react" +import {Typography, Space, Button} from "antd" +import {ArrowLeft, Check, Copy, Play} from "@phosphor-icons/react" import {createUseStyles} from "react-jss" -import YouTube, {YouTubeProps} from "react-youtube" +import {JSSTheme} from "@/lib/Types" import {isDemo} from "@/lib/helpers/utils" -import {StyleProps} from "@/lib/Types" +import {useRouter} from "next/router" +import {useState} from "react" -const useStyles = createUseStyles({ - modal: ({themeMode}: StyleProps) => ({ - "& .ant-modal-content": { - backgroundColor: themeMode === "dark" ? "rgb(13, 17, 23)" : "#fff", - }, - "& .ant-modal-header": { - backgroundColor: themeMode === "dark" ? "rgb(13, 17, 23)" : "#fff", - }, - "& .ant-modal-close": { - top: 23, - }, - "& .ant-modal": { - width: "auto !important", - }, - "& .ant-modal-body": { - display: "flex", - alignItems: "center", - gap: 10, - }, - }), - title: { - margin: 0, - }, - body: { - marginTop: 16, - height: 360, +const {Text} = Typography + +const useStyles = createUseStyles((theme: JSSTheme) => ({ + modal: { + display: "flex", + flexDirection: "column", + gap: 24, }, - wrapper: { - width: 450, - marginRight: 10, - "& ol": { - padding: "0 5px", - listStyleType: "none", + headerText: { + "& .ant-typography": { + lineHeight: theme.lineHeightLG, + fontSize: theme.fontSizeHeading4, + fontWeight: theme.fontWeightStrong, }, }, - copyBtn: { - backgroundColor: "transparent", - alignSelf: "flex-start", + label: { + fontWeight: theme.fontWeightMedium, }, - container: { - margin: "20px 0", - "& li": { - fontSize: 16, - fontWeight: 600, - marginBottom: 3, - }, + command: { + width: "92%", + backgroundColor: "#f6f8fa", + borderRadius: theme.borderRadius, + border: "1px solid", + borderColor: theme.colorBorder, + color: theme.colorText, + cursor: "default", + padding: "3.5px 11px", }, - command: ({themeMode}: StyleProps) => ({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: themeMode === "light" ? "#f6f8fa" : "#161b22", - padding: "5px 10px", - borderRadius: 5, - border: `1px solid ${themeMode === "light" ? "#d0d7de" : "#30363d"}`, - color: themeMode === "light" ? "#1f2328" : "#e6edf3", - "& span": { - letterSpacing: 0.3, - }, - }), - youtube: { - "& iframe": { - height: 430, - width: 560, + copyBtn: { + width: theme.controlHeight, + height: theme.controlHeight, + "& > .ant-btn-icon": { + marginTop: 2, + marginLeft: 1, }, }, -}) +})) -const {Title} = Typography +type Props = { + setCurrent: React.Dispatch> + hasAvailableApps: boolean +} -type Props = React.ComponentProps & {} +const WriteOwnAppModal = ({setCurrent, hasAvailableApps}: Props) => { + const classes = useStyles() + const router = useRouter() + const [isCopied, setIsCopied] = useState(null) -const WriteOwnAppModal: React.FC = ({...props}) => { - const {appTheme} = useAppTheme() - const classes = useStyles({themeMode: appTheme} as StyleProps) - const youtubePlayer = useRef(null) + const onCopyCode = (code: string, index: number) => { + navigator.clipboard.writeText(code) + setIsCopied(index) - const onPlayerReady: YouTubeProps["onStateChange"] = (event) => { - if (!props.open) { - event.target.pauseVideo() - } + setTimeout(() => setIsCopied(null), 2000) } - - useEffect(() => { - if (!props.open && youtubePlayer.current) { - youtubePlayer.current.getInternalPlayer().pauseVideo() - } - }, [props.open]) - + const listOfCommands = [ + ...(isDemo() + ? [ + { + title: "Add an API Key", + code: "", + }, + ] + : []), + { + title: "Install Agenta AI", + code: "pip install -U agenta", + }, + { + title: "Clone the example application", + code: "git clone https://github.com/Agenta-AI/simple_prompt && cd simple_prompt", + }, + { + title: "Set up environement variable", + code: 'echo -e "OPENAI_API_KEY=sk-xxx" > .env', + }, + { + title: "Setup Agenta (select start from blank)", + code: "agenta init", + }, + { + title: "Serve an app variant", + code: "agenta variant serve --file_name app.py", + }, + ] return ( - - Write Your Own App - - } - {...props} - afterClose={() => { - if (youtubePlayer.current) { - youtubePlayer.current.getInternalPlayer().stopVideo() - } - }} - > -
-
    - {isDemo() && ( -
    -
  1. - 0. Get an API key -
  2. -
    +
    +
    + + {hasAvailableApps && ( +
+ Write your own app + + + + +
- { - youtubePlayer.current = youtube - }} - className={classes.youtube} - /> -
+ + + Create your own complex application using any framework. To learn more about how to + write your own LLM app here,{" "} + + Click here + + + + {listOfCommands.map((item, ind) => ( +
+
+ + Step {ind + 1}: {item.title} + + + {item.title.includes("API Key") ? ( + + ) : ( +
+
{item.code}
+ +
+ )} +
+
+ ))} + ) } diff --git a/agenta-web/src/styles/globals.css b/agenta-web/src/styles/globals.css index 19a9ec3ef9..89bb5e3723 100644 --- a/agenta-web/src/styles/globals.css +++ b/agenta-web/src/styles/globals.css @@ -94,3 +94,43 @@ body { justify-content: center; margin: 0 auto; } + +/* Custom loading circle for app creation proccess */ +.loading-circle { + display: flex; + justify-content: center; + align-items: center; + width: 160px; + height: 160px; +} +.loading-circle path { + stroke-width: 2; + animation: colorChange 1.6s infinite; + transition: + stroke 0.4s ease, + stroke-width 0.4s ease; + transition-duration: 0.5s; +} + +.loading-circle path:nth-child(1) { + animation-delay: 0s; +} +.loading-circle path:nth-child(2) { + animation-delay: 0.4s; +} +.loading-circle path:nth-child(3) { + animation-delay: 0.8s; +} +.loading-circle path:nth-child(4) { + animation-delay: 1.2s; +} +@keyframes colorChange { + 0%, + 25% { + stroke: #36cfc9; + } + 26%, + 100% { + stroke: #d6dee6; + } +}