Spaces:
Build error
Build error
import { useDisclosure } from "@heroui/react"; | |
import React from "react"; | |
import { useNavigate } from "react-router"; | |
import { useDispatch, useSelector } from "react-redux"; | |
import { FaServer, FaExternalLinkAlt } from "react-icons/fa"; | |
import { useTranslation } from "react-i18next"; | |
import { DiGit } from "react-icons/di"; | |
import { VscCode } from "react-icons/vsc"; | |
import { I18nKey } from "#/i18n/declaration"; | |
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state"; | |
import { useConversationId } from "#/hooks/use-conversation-id"; | |
import { Controls } from "#/components/features/controls/controls"; | |
import { clearTerminal } from "#/state/command-slice"; | |
import { useEffectOnce } from "#/hooks/use-effect-once"; | |
import GlobeIcon from "#/icons/globe.svg?react"; | |
import JupyterIcon from "#/icons/jupyter.svg?react"; | |
import TerminalIcon from "#/icons/terminal.svg?react"; | |
import { clearJupyter } from "#/state/jupyter-slice"; | |
import { ChatInterface } from "../components/features/chat/chat-interface"; | |
import { WsClientProvider } from "#/context/ws-client-provider"; | |
import { EventHandler } from "../wrapper/event-handler"; | |
import { useConversationConfig } from "#/hooks/query/use-conversation-config"; | |
import { Container } from "#/components/layout/container"; | |
import { | |
Orientation, | |
ResizablePanel, | |
} from "#/components/layout/resizable-panel"; | |
import Security from "#/components/shared/modals/security/security"; | |
import { useActiveConversation } from "#/hooks/query/use-active-conversation"; | |
import { ServedAppLabel } from "#/components/layout/served-app-label"; | |
import { useSettings } from "#/hooks/query/use-settings"; | |
import { RootState } from "#/store"; | |
import { displayErrorToast } from "#/utils/custom-toast-handlers"; | |
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state"; | |
import { transformVSCodeUrl } from "#/utils/vscode-url-helper"; | |
import OpenHands from "#/api/open-hands"; | |
import { TabContent } from "#/components/layout/tab-content"; | |
import { useIsAuthed } from "#/hooks/query/use-is-authed"; | |
function AppContent() { | |
useConversationConfig(); | |
const { t } = useTranslation(); | |
const { data: settings } = useSettings(); | |
const { conversationId } = useConversationId(); | |
const { data: conversation, isFetched, refetch } = useActiveConversation(); | |
const { data: isAuthed } = useIsAuthed(); | |
const { curAgentState } = useSelector((state: RootState) => state.agent); | |
const dispatch = useDispatch(); | |
const navigate = useNavigate(); | |
// Set the document title to the conversation title when available | |
useDocumentTitleFromState(); | |
const [width, setWidth] = React.useState(window.innerWidth); | |
React.useEffect(() => { | |
if (isFetched && !conversation && isAuthed) { | |
displayErrorToast( | |
"This conversation does not exist, or you do not have permission to access it.", | |
); | |
navigate("/"); | |
} else if (conversation?.status === "STOPPED") { | |
// start the conversation if the state is stopped on initial load | |
OpenHands.startConversation(conversation.conversation_id).then(() => | |
refetch(), | |
); | |
} | |
}, [conversation?.conversation_id, isFetched, isAuthed]); | |
React.useEffect(() => { | |
dispatch(clearTerminal()); | |
dispatch(clearJupyter()); | |
}, [conversationId]); | |
useEffectOnce(() => { | |
dispatch(clearTerminal()); | |
dispatch(clearJupyter()); | |
}); | |
function handleResize() { | |
setWidth(window.innerWidth); | |
} | |
React.useEffect(() => { | |
window.addEventListener("resize", handleResize); | |
return () => { | |
window.removeEventListener("resize", handleResize); | |
}; | |
}, []); | |
const { | |
isOpen: securityModalIsOpen, | |
onOpen: onSecurityModalOpen, | |
onOpenChange: onSecurityModalOpenChange, | |
} = useDisclosure(); | |
function renderMain() { | |
const basePath = `/conversations/${conversationId}`; | |
if (width <= 640) { | |
return ( | |
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary"> | |
<ChatInterface /> | |
</div> | |
); | |
} | |
return ( | |
<ResizablePanel | |
orientation={Orientation.HORIZONTAL} | |
className="grow h-full min-h-0 min-w-0" | |
initialSize={500} | |
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary" | |
secondClassName="flex flex-col overflow-hidden" | |
firstChild={<ChatInterface />} | |
secondChild={ | |
<Container | |
className="h-full w-full" | |
labels={[ | |
{ | |
label: "Changes", | |
to: "", | |
icon: <DiGit className="w-6 h-6" />, | |
}, | |
{ | |
label: ( | |
<div className="flex items-center gap-1"> | |
{t(I18nKey.VSCODE$TITLE)} | |
</div> | |
), | |
to: "vscode", | |
icon: <VscCode className="w-5 h-5" />, | |
rightContent: !RUNTIME_INACTIVE_STATES.includes( | |
curAgentState, | |
) ? ( | |
<FaExternalLinkAlt | |
className="w-3 h-3 text-neutral-400 cursor-pointer" | |
onClick={async (e) => { | |
e.preventDefault(); | |
e.stopPropagation(); | |
if (conversationId) { | |
try { | |
const data = | |
await OpenHands.getVSCodeUrl(conversationId); | |
if (data.vscode_url) { | |
const transformedUrl = transformVSCodeUrl( | |
data.vscode_url, | |
); | |
if (transformedUrl) { | |
window.open(transformedUrl, "_blank"); | |
} | |
} | |
} catch (err) { | |
// Silently handle the error | |
} | |
} | |
}} | |
/> | |
) : null, | |
}, | |
{ | |
label: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL), | |
to: "terminal", | |
icon: <TerminalIcon />, | |
}, | |
{ label: "Jupyter", to: "jupyter", icon: <JupyterIcon /> }, | |
{ | |
label: <ServedAppLabel />, | |
to: "served", | |
icon: <FaServer />, | |
}, | |
{ | |
label: ( | |
<div className="flex items-center gap-1"> | |
{t(I18nKey.BROWSER$TITLE)} | |
</div> | |
), | |
to: "browser", | |
icon: <GlobeIcon />, | |
}, | |
]} | |
> | |
{/* Use both Outlet and TabContent */} | |
<div className="h-full w-full"> | |
<TabContent conversationPath={basePath} /> | |
</div> | |
</Container> | |
} | |
/> | |
); | |
} | |
return ( | |
<WsClientProvider conversationId={conversationId}> | |
<EventHandler> | |
<div data-testid="app-route" className="flex flex-col h-full gap-3"> | |
<div className="flex h-full overflow-auto">{renderMain()}</div> | |
<Controls | |
setSecurityOpen={onSecurityModalOpen} | |
showSecurityLock={!!settings?.SECURITY_ANALYZER} | |
/> | |
{settings && ( | |
<Security | |
isOpen={securityModalIsOpen} | |
onOpenChange={onSecurityModalOpenChange} | |
securityAnalyzer={settings.SECURITY_ANALYZER} | |
/> | |
)} | |
</div> | |
</EventHandler> | |
</WsClientProvider> | |
); | |
} | |
function App() { | |
return <AppContent />; | |
} | |
export default App; | |