Просмотр исходного кода

feat: add change indicator to workspace tabs (#1236)

Alex Bäuerle 1 год назад
Родитель
Сommit
49ff317b50

+ 44 - 1
frontend/src/components/Workspace.tsx

@@ -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>
               }
             />

+ 9 - 7
frontend/src/state/browserSlice.ts

@@ -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;

+ 7 - 5
frontend/src/state/codeSlice.ts

@@ -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;