diff --git a/plane-app/plane.env b/plane-app/plane.env index 02f4f79..76e22e9 100644 --- a/plane-app/plane.env +++ b/plane-app/plane.env @@ -120,3 +120,6 @@ PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS=3 PLANE_NODEDC_AGENT_GATEWAY_URL=http://host.docker.internal:4100 PLANE_NODEDC_AGENT_GATEWAY_TOKEN=local-dev-codex-agent-gateway-token-change-me PLANE_NODEDC_AGENT_GATEWAY_TIMEOUT_SECONDS=5 +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL=http://host.docker.internal:18082 +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN=dev-ai-workspace-assistant-token +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS=8 diff --git a/plane-app/plane.env.example b/plane-app/plane.env.example index 9b5b81b..03dbdb0 100644 --- a/plane-app/plane.env.example +++ b/plane-app/plane.env.example @@ -88,3 +88,8 @@ LIVE_SERVER_SECRET_KEY=CHANGE_ME_BEFORE_PRODUCTION DOCKERHUB_USER=makeplane PULL_POLICY=if_not_present CUSTOM_BUILD=false + +# NODE.DC AI Workspace Assistant +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL= +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN= +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS=8 diff --git a/plane-app/plane.env.staging.example b/plane-app/plane.env.staging.example index ef26f55..867ed0a 100644 --- a/plane-app/plane.env.staging.example +++ b/plane-app/plane.env.staging.example @@ -97,4 +97,7 @@ PLANE_NODEDC_WORKSPACE_POLICY_TIMEOUT_SECONDS=3 PLANE_NODEDC_AGENT_GATEWAY_URL=https://codex-api.staging.nodedc.example PLANE_NODEDC_AGENT_GATEWAY_TOKEN=replace-with-codex-agent-gateway-internal-token PLANE_NODEDC_AGENT_GATEWAY_TIMEOUT_SECONDS=5 +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_URL=http://ai-workspace-assistant:18082 +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TOKEN=replace-with-ai-workspace-assistant-token +PLANE_NODEDC_AI_WORKSPACE_ASSISTANT_TIMEOUT_SECONDS=8 PLANE_NODEDC_WORKSPACE_CREATION_MODE=any_authorized_user diff --git a/plane-src/apps/web/ce/components/workspace/content-wrapper.tsx b/plane-src/apps/web/ce/components/workspace/content-wrapper.tsx index 5f3d417..121013e 100644 --- a/plane-src/apps/web/ce/components/workspace/content-wrapper.tsx +++ b/plane-src/apps/web/ce/components/workspace/content-wrapper.tsx @@ -10,6 +10,7 @@ import { observer } from "mobx-react"; import { cn } from "@plane/utils"; import { AppRailRoot } from "@/components/navigation"; import { useAppRailVisibility } from "@/lib/app-rail"; +import { AIWorkspaceGlobalControl } from "@/components/ai-workspace/global-control"; import { VoiceTaskerGlobalControl } from "@/components/voice-tasker/global-control"; // local imports import { TopNavigationRoot } from "../navigations"; @@ -40,6 +41,7 @@ export const WorkspaceContentWrapper = observer(function WorkspaceContentWrapper > {children} + {workspaceSlug && } {workspaceSlug && } diff --git a/plane-src/apps/web/core/components/ai-workspace/global-control.tsx b/plane-src/apps/web/core/components/ai-workspace/global-control.tsx new file mode 100644 index 0000000..56de57b --- /dev/null +++ b/plane-src/apps/web/core/components/ai-workspace/global-control.tsx @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import useSWR from "swr"; +import { CheckCircle2, MessageCircle, Monitor, RefreshCw, X } from "lucide-react"; +// plane imports +import { Button } from "@plane/propel/button"; +import { Tooltip } from "@plane/propel/tooltip"; +import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; +import { cn } from "@plane/utils"; +// services +import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service"; + +const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService(); + +const aiWorkspaceCloseButtonClassName = + "absolute top-0.5 right-0.5 flex h-12 w-12 items-center justify-center rounded-full border-0 bg-[#17181B] text-white shadow-none ring-0 transition-transform outline-none hover:scale-[1.03] hover:bg-[#0F1012]"; + +type TAIWorkspaceGlobalControlProps = { + workspaceSlug: string; +}; + +export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) { + const [isOpen, setIsOpen] = useState(false); + const [dockSlot, setDockSlot] = useState(null); + + const { data, error, isLoading, mutate } = useSWR( + isOpen && workspaceSlug ? `AI_WORKSPACE_EXECUTORS_${workspaceSlug}` : null, + () => workspaceAIWorkspaceService.listExecutors(workspaceSlug), + { refreshInterval: isOpen ? 10000 : 0 } + ); + + useEffect(() => { + if (typeof document === "undefined") return; + + const updateDockSlot = () => { + setDockSlot( + document.querySelector("[data-nodedc-voice-task-toolbar-slot]") ?? + document.querySelector("[data-nodedc-voice-task-dock-slot]") + ); + }; + + updateDockSlot(); + const observer = new MutationObserver(updateDockSlot); + observer.observe(document.body, { childList: true, subtree: true }); + + return () => { + observer.disconnect(); + }; + }, []); + + const selectedExecutor = useMemo( + () => data?.executors?.find((executor) => executor.id === data.selectedExecutorId) ?? data?.executors?.[0] ?? null, + [data?.executors, data?.selectedExecutorId] + ); + + const handleOpen = useCallback(() => { + setIsOpen(true); + }, []); + + const handleClose = useCallback(() => { + setIsOpen(false); + }, []); + + return ( + <> + {dockSlot + ? createPortal( + + + , + dockSlot + ) + : null} + + +
+
+
+

AI Workspace

+
Ops
+
+ +
+ +
+
+
+ + + +
+
+ {selectedExecutor?.name ?? "Codex assistant"} +
+
+ {selectedExecutor?.model || selectedExecutor?.accountLabel || "AI Workspace"} +
+
+
+ +
+ +
+ {error ? ( +
+ {error?.message || error?.error || "AI Workspace недоступен"} +
+ ) : isLoading ? ( +
Загрузка
+ ) : data?.executors?.length ? ( + data.executors.map((executor) => ( +
+
+ +
+
{executor.name}
+
+ {executor.connectionMode === "hub" ? executor.pairingCode || "hub" : "direct"} +
+
+
+ + + {executor.status} + +
+ )) + ) : ( +
+ Устройства не подключены +
+ )} +
+
+
+
+ + ); +} diff --git a/plane-src/apps/web/core/services/workspace-ai-workspace.service.ts b/plane-src/apps/web/core/services/workspace-ai-workspace.service.ts new file mode 100644 index 0000000..bca90bf --- /dev/null +++ b/plane-src/apps/web/core/services/workspace-ai-workspace.service.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { API_BASE_URL } from "@plane/constants"; +import { APIService } from "@/services/api.service"; + +export type TAIWorkspaceExecutorStatus = "unknown" | "online" | "offline" | "checking" | "error"; + +export type TAIWorkspaceExecutor = { + id: string; + name: string; + type: string; + connectionMode: "hub" | "direct"; + pairingCode?: string | null; + model?: string | null; + accountLabel?: string | null; + status: TAIWorkspaceExecutorStatus; + lastSeenAt?: string | null; + updatedAt?: string | null; +}; + +export type TAIWorkspaceExecutorListResponse = { + ok: boolean; + selectedExecutorId?: string | null; + executors: TAIWorkspaceExecutor[]; +}; + +export class WorkspaceAIWorkspaceService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async listExecutors(workspaceSlug: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/ai-workspace/executors/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data ?? error; + }); + } +} diff --git a/plane-src/apps/web/styles/globals.css b/plane-src/apps/web/styles/globals.css index 837c9e9..7b0c49d 100644 --- a/plane-src/apps/web/styles/globals.css +++ b/plane-src/apps/web/styles/globals.css @@ -627,6 +627,7 @@ display: flex; flex: 0 0 auto; align-items: center; + gap: 0.5rem; justify-content: center; min-width: 0; } @@ -1648,10 +1649,12 @@ } .nodedc-expanded-tool-slot { - display: grid; + display: flex; + align-items: center; + gap: 0.5rem; + justify-content: center; min-height: 3rem; min-width: 3rem; - place-items: center; } .nodedc-expanded-header-filters-slot,