mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat: add change indicator to workspace tabs (#1236)
This commit is contained in:
parent
6340cd9aed
commit
49ff317b50
@ -1,10 +1,14 @@
|
||||
import { Tab, Tabs } from "@nextui-org/react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IoIosGlobe } from "react-icons/io";
|
||||
import { VscCode } from "react-icons/vsc";
|
||||
import { useSelector } from "react-redux";
|
||||
import Calendar from "../assets/calendar";
|
||||
import { I18nKey } from "../i18n/declaration";
|
||||
import { initialState as initialBrowserState } from "../state/browserSlice";
|
||||
import { initialState as initialCodeState } from "../state/codeSlice";
|
||||
import { RootState } from "../store";
|
||||
import { AllTabs, TabOption, TabType } from "../types/TabOption";
|
||||
import Browser from "./Browser";
|
||||
import CodeEditor from "./CodeEditor";
|
||||
@ -12,7 +16,18 @@ import Planner from "./Planner";
|
||||
|
||||
function Workspace() {
|
||||
const { t } = useTranslation();
|
||||
const plan = useSelector((state: RootState) => state.plan.plan);
|
||||
const code = useSelector((state: RootState) => state.code.code);
|
||||
const screenshotSrc = useSelector(
|
||||
(state: RootState) => state.browser.screenshotSrc,
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabType>(TabOption.CODE);
|
||||
const [changes, setChanges] = useState<Record<TabType, boolean>>({
|
||||
[TabOption.PLANNER]: false,
|
||||
[TabOption.CODE]: false,
|
||||
[TabOption.BROWSER]: false,
|
||||
});
|
||||
|
||||
const tabData = useMemo(
|
||||
() => ({
|
||||
@ -35,6 +50,30 @@ function Workspace() {
|
||||
[t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== TabOption.PLANNER && plan.mainGoal !== undefined) {
|
||||
setChanges((prev) => ({ ...prev, [TabOption.PLANNER]: true }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [plan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== TabOption.CODE && code !== initialCodeState.code) {
|
||||
setChanges((prev) => ({ ...prev, [TabOption.CODE]: true }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [plan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activeTab !== TabOption.BROWSER &&
|
||||
screenshotSrc !== initialBrowserState.screenshotSrc
|
||||
) {
|
||||
setChanges((prev) => ({ ...prev, [TabOption.BROWSER]: true }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [screenshotSrc]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-0 grow">
|
||||
<div
|
||||
@ -52,6 +91,7 @@ function Workspace() {
|
||||
}}
|
||||
size="lg"
|
||||
onSelectionChange={(v) => {
|
||||
setChanges((prev) => ({ ...prev, [v as TabType]: false }));
|
||||
setActiveTab(v as TabType);
|
||||
}}
|
||||
>
|
||||
@ -63,6 +103,9 @@ function Workspace() {
|
||||
<div className="flex grow items-center gap-2 justify-center text-xs">
|
||||
{tabData[tab].icon}
|
||||
<span>{tabData[tab].name}</span>
|
||||
{changes[tab] && (
|
||||
<div className="w-2 h-2 rounded-full animate-pulse bg-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
export const initialState = {
|
||||
// URL of browser window (placeholder for now, will be replaced with the actual URL later)
|
||||
url: "https://github.com/OpenDevin/OpenDevin",
|
||||
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
|
||||
screenshotSrc:
|
||||
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
};
|
||||
|
||||
export const browserSlice = createSlice({
|
||||
name: "browser",
|
||||
initialState: {
|
||||
// URL of browser window (placeholder for now, will be replaced with the actual URL later)
|
||||
url: "https://github.com/OpenDevin/OpenDevin",
|
||||
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
|
||||
screenshotSrc:
|
||||
"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
},
|
||||
initialState,
|
||||
reducers: {
|
||||
setUrl: (state, action) => {
|
||||
state.url = action.payload;
|
||||
|
||||
@ -3,13 +3,15 @@ import { INode, flattenTree } from "react-accessible-treeview";
|
||||
import { IFlatMetadata } from "react-accessible-treeview/dist/TreeView/utils";
|
||||
import { WorkspaceFile } from "../services/fileService";
|
||||
|
||||
export const initialState = {
|
||||
code: "# Welcome to OpenDevin!",
|
||||
selectedIds: [] as number[],
|
||||
workspaceFolder: { name: "" } as WorkspaceFile,
|
||||
};
|
||||
|
||||
export const codeSlice = createSlice({
|
||||
name: "code",
|
||||
initialState: {
|
||||
code: "# Welcome to OpenDevin!",
|
||||
selectedIds: [] as number[],
|
||||
workspaceFolder: { name: "" } as WorkspaceFile,
|
||||
},
|
||||
initialState,
|
||||
reducers: {
|
||||
setCode: (state, action) => {
|
||||
state.code = action.payload;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user