fix: align ops ai workspace settings UI
This commit is contained in:
@@ -8,11 +8,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import type { FormEvent, MouseEvent } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import useSWR from "swr";
|
||||
import { CheckCircle2, Download, MessageCircle, Monitor, Plus, RefreshCw, Settings, Trash2, X } from "lucide-react";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
// plane imports
|
||||
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";
|
||||
import type {
|
||||
@@ -24,14 +23,14 @@ import type {
|
||||
const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService();
|
||||
const DEFAULT_WINDOWS_AGENT_PORT = "8787";
|
||||
const capabilityOptions = [
|
||||
{ value: "chat", label: "Chat" },
|
||||
{ value: "code", label: "Code" },
|
||||
{ value: "engine", label: "Engine" },
|
||||
{ value: "ops", label: "Ops" },
|
||||
{ value: "ndc-agent-core", label: "NDC Agent" },
|
||||
{ value: "mcp", label: "MCP" },
|
||||
{ value: "workflows", label: "Workflows" },
|
||||
];
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -67,7 +66,8 @@ function draftFromExecutor(executor: TAIWorkspaceExecutor | null | undefined): T
|
||||
workspacePath: executor.workspacePath || "",
|
||||
agentPort: String(executor.agentPort || DEFAULT_WINDOWS_AGENT_PORT),
|
||||
accountLabel: executor.accountLabel || "",
|
||||
capabilities: Array.isArray(executor.capabilities) && executor.capabilities.length ? executor.capabilities : ["ops"],
|
||||
capabilities:
|
||||
Array.isArray(executor.capabilities) && executor.capabilities.length ? executor.capabilities : ["ops"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@ function toInput(draft: TExecutorDraft): TAIWorkspaceExecutorInput {
|
||||
}
|
||||
|
||||
function statusLabel(status: string | null | undefined) {
|
||||
if (status === "online") return "online";
|
||||
if (status === "offline") return "offline";
|
||||
if (status === "checking") return "checking";
|
||||
if (status === "error") return "error";
|
||||
return "unknown";
|
||||
if (status === "online") return "Online";
|
||||
if (status === "offline") return "Offline";
|
||||
if (status === "checking") return "Checking";
|
||||
if (status === "error") return "Error";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) {
|
||||
@@ -157,22 +157,21 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||
setActionError("");
|
||||
}, []);
|
||||
|
||||
const runMutation = useCallback(
|
||||
async (id: string, action: () => Promise<unknown>, successText: string) => {
|
||||
setBusyId(id);
|
||||
setActionError("");
|
||||
setNotice("");
|
||||
try {
|
||||
await action();
|
||||
setNotice(successText);
|
||||
} catch (mutationError: any) {
|
||||
setActionError(String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed"));
|
||||
} finally {
|
||||
setBusyId("");
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
const runMutation = useCallback(async (id: string, action: () => Promise<unknown>, successText: string) => {
|
||||
setBusyId(id);
|
||||
setActionError("");
|
||||
setNotice("");
|
||||
try {
|
||||
await action();
|
||||
setNotice(successText);
|
||||
} catch (mutationError: any) {
|
||||
setActionError(
|
||||
String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed")
|
||||
);
|
||||
} finally {
|
||||
setBusyId("");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const startNew = () => {
|
||||
setDetailsMode("new");
|
||||
@@ -215,30 +214,43 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||
return;
|
||||
}
|
||||
if (detailsMode === "new") {
|
||||
await runMutation("save", async () => {
|
||||
const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input);
|
||||
await mutate(payload, { revalidate: false });
|
||||
const next = payload.executors.find((executor) => executor.name === input.name) ?? payload.executors[0] ?? null;
|
||||
setEditingId(next?.id || "");
|
||||
setDetailsMode(next ? "edit" : "context");
|
||||
}, "Устройство создано");
|
||||
await runMutation(
|
||||
"save",
|
||||
async () => {
|
||||
const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input);
|
||||
await mutate(payload, { revalidate: false });
|
||||
const next =
|
||||
payload.executors.find((executor) => executor.name === input.name) ?? payload.executors[0] ?? null;
|
||||
setEditingId(next?.id || "");
|
||||
setDetailsMode(next ? "edit" : "context");
|
||||
},
|
||||
"Устройство создано"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!editingExecutor) return;
|
||||
await runMutation("save", async () => {
|
||||
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input);
|
||||
await mutate(payload, { revalidate: false });
|
||||
setDetailsMode("context");
|
||||
}, "Устройство сохранено");
|
||||
await runMutation(
|
||||
"save",
|
||||
async () => {
|
||||
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input);
|
||||
await mutate(payload, { revalidate: false });
|
||||
setDetailsMode("context");
|
||||
},
|
||||
"Устройство сохранено"
|
||||
);
|
||||
};
|
||||
|
||||
const deleteEditingExecutor = async () => {
|
||||
if (!editingExecutor) return;
|
||||
await runMutation(`delete:${editingExecutor.id}`, async () => {
|
||||
const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id);
|
||||
await mutate(payload, { revalidate: false });
|
||||
setDetailsMode("context");
|
||||
}, "Устройство удалено");
|
||||
await runMutation(
|
||||
`delete:${editingExecutor.id}`,
|
||||
async () => {
|
||||
const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id);
|
||||
await mutate(payload, { revalidate: false });
|
||||
setDetailsMode("context");
|
||||
},
|
||||
"Устройство удалено"
|
||||
);
|
||||
};
|
||||
|
||||
const toggleCapability = (value: string) => {
|
||||
@@ -253,13 +265,25 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||
const downloadAgent = async (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
if (!editingExecutor) return;
|
||||
event.preventDefault();
|
||||
await runMutation("download", async () => {
|
||||
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, toInput(draft));
|
||||
await mutate(payload, { revalidate: false });
|
||||
window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, editingExecutor.id, {
|
||||
port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT,
|
||||
});
|
||||
}, "Installer подготовлен");
|
||||
await runMutation(
|
||||
"download",
|
||||
async () => {
|
||||
const payload = await workspaceAIWorkspaceService.updateExecutor(
|
||||
workspaceSlug,
|
||||
editingExecutor.id,
|
||||
toInput(draft)
|
||||
);
|
||||
await mutate(payload, { revalidate: false });
|
||||
window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(
|
||||
workspaceSlug,
|
||||
editingExecutor.id,
|
||||
{
|
||||
port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT,
|
||||
}
|
||||
);
|
||||
},
|
||||
"Installer подготовлен"
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -287,214 +311,301 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||
width={EModalWidth.XXL}
|
||||
className="overflow-visible"
|
||||
>
|
||||
<div className="nodedc-ai-workspace-modal relative">
|
||||
<div className="nodedc-ai-workspace-modal__head">
|
||||
<div>
|
||||
<div className="nodedc-ai-workspace-kicker">AI Space</div>
|
||||
<h3>AI Workspace</h3>
|
||||
<div className="nodedc-glass-modal ai-settings-modal ai-settings-modal--workspace nodedc-ai-workspace-product-modal">
|
||||
<div className="nodedc-glass-modal__head">
|
||||
<div className="nodedc-glass-modal__titles">
|
||||
<div className="nodedc-glass-modal__title">AI Workspace Settings</div>
|
||||
<div className="nodedc-glass-modal__accent-title" title="MODEL CONTEXT">
|
||||
MODEL CONTEXT
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className={aiWorkspaceCloseButtonClassName} onClick={handleClose} aria-label="Закрыть AI Workspace">
|
||||
<X className="size-5" />
|
||||
<button type="button" className="nodedc-glass-modal__close" onClick={handleClose} aria-label="Закрыть">
|
||||
<span className="nodedc-glass-modal__close-mark" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-ai-settings-workspace" data-details-open={detailsOpen ? "true" : undefined}>
|
||||
<aside className="nodedc-ai-settings-device-list">
|
||||
<div className="nodedc-ai-settings-devices-head">
|
||||
<div>
|
||||
<div className="nodedc-ai-workspace-kicker">Devices</div>
|
||||
<div className="nodedc-ai-settings-devices-title">Устройства</div>
|
||||
</div>
|
||||
<button type="button" className="nodedc-ai-icon-button" aria-label="Подключить устройство" onClick={startNew}>
|
||||
<Plus className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isLoading ? <div className="nodedc-ai-settings-empty-list">Загрузка</div> : null}
|
||||
{executors.map((executor) => (
|
||||
<button
|
||||
key={executor.id}
|
||||
type="button"
|
||||
className="nodedc-ai-settings-executor-row"
|
||||
data-active={activeExecutor?.id === executor.id && detailsMode !== "new" ? "true" : undefined}
|
||||
onClick={() => selectExecutor(executor)}
|
||||
>
|
||||
<span>
|
||||
<Monitor className="size-4" />
|
||||
<b>{executor.name}</b>
|
||||
</span>
|
||||
<small>
|
||||
{executor.connectionMode === "hub" ? executor.pairingCode || "Cloud Hub" : "Direct"} · {statusLabel(executor.status)}
|
||||
</small>
|
||||
</button>
|
||||
))}
|
||||
{!isLoading && !executors.length ? (
|
||||
<div className="nodedc-ai-settings-empty-list">
|
||||
<span>{error ? "Registry недоступен" : "Устройств нет"}</span>
|
||||
<small>{error ? String((error as any)?.message || (error as any)?.error || error) : "Подключенные устройства появятся здесь."}</small>
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
|
||||
<section className="nodedc-ai-settings-context-pane">
|
||||
<div className="nodedc-ai-settings-form-top">
|
||||
<div>
|
||||
<div className="nodedc-ai-settings-section-title">{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}</div>
|
||||
<div className="nodedc-ai-settings-subtitle">Общий пользовательский ассистент для Ops и Engine</div>
|
||||
</div>
|
||||
{activeExecutor ? <span className="nodedc-ai-status-pill">{statusLabel(activeExecutor.status)}</span> : null}
|
||||
</div>
|
||||
|
||||
{activeExecutor && detailsMode !== "new" ? (
|
||||
<>
|
||||
<div className="nodedc-ai-settings-active-model">
|
||||
<span>Активное устройство</span>
|
||||
<b>{activeExecutor.name}</b>
|
||||
<small>{activeExecutor.connectionMode === "hub" ? activeExecutor.pairingCode || "Cloud Hub" : activeExecutor.endpoint}</small>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-ai-settings-note" data-ready="true">
|
||||
Это устройство сохранено в общем AI Workspace registry. Оно будет видно в Engine и Ops для этого же пользователя.
|
||||
</div>
|
||||
|
||||
<div className="nodedc-ai-settings-actions">
|
||||
<button type="button" className="modal-btn" disabled={isBusy} onClick={() => void mutate()}>
|
||||
<RefreshCw className={cn("size-4", isLoading && "animate-spin")} />
|
||||
Обновить
|
||||
</button>
|
||||
<button type="button" className="modal-btn" disabled={isBusy} onClick={editActive}>
|
||||
<Settings className="size-4" />
|
||||
Настройки
|
||||
</button>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={handleClose}>
|
||||
<CheckCircle2 className="size-4" />
|
||||
Готово
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : detailsMode === "new" ? (
|
||||
<div className="nodedc-ai-settings-empty-context">
|
||||
<span>Новое устройство</span>
|
||||
<small>Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer агента.</small>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="nodedc-ai-settings-empty-context" data-tone="error">
|
||||
<span>Registry backend недоступен</span>
|
||||
<small>{String((error as any)?.message || (error as any)?.error || error)}</small>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={() => void mutate()}>
|
||||
Повторить
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="nodedc-ai-settings-empty-context">
|
||||
<span>Устройство не выбрано</span>
|
||||
<small>Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически.</small>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={startNew}>
|
||||
Подключить устройство
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notice ? <div className="nodedc-ai-settings-note" data-tone="success">{notice}</div> : null}
|
||||
{actionError ? <div className="nodedc-ai-settings-error">{actionError}</div> : null}
|
||||
</section>
|
||||
|
||||
{detailsOpen ? (
|
||||
<form className="nodedc-ai-settings-details-pane" onSubmit={saveDraft}>
|
||||
<div className="nodedc-ai-settings-form-top">
|
||||
<div className="nodedc-glass-modal__body">
|
||||
<div className="ai-settings-workspace" data-details-open={detailsOpen ? "true" : undefined}>
|
||||
<aside className="ai-settings-model-list">
|
||||
<div className="ai-settings-devices-head">
|
||||
<div>
|
||||
<div className="nodedc-ai-settings-section-title">{detailsMode === "new" ? "Подключить устройство" : "Параметры устройства"}</div>
|
||||
<div className="nodedc-ai-settings-subtitle">Cloud Hub или прямой endpoint</div>
|
||||
<div className="ai-console-kicker">AI Space</div>
|
||||
<div className="ai-settings-devices-title">Устройства</div>
|
||||
</div>
|
||||
{detailsMode === "edit" ? (
|
||||
<button type="button" className="nodedc-ai-mini-close" aria-label="Закрыть параметры" onClick={() => setDetailsMode("context")}>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Имя устройства</span>
|
||||
<input value={draft.name} placeholder="MACPRO Codex" onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} />
|
||||
</label>
|
||||
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Подключение</span>
|
||||
<select value={draft.connectionMode} onChange={(event) => setDraft((current) => ({ ...current, connectionMode: event.target.value as TAIWorkspaceExecutorConnectionMode }))}>
|
||||
<option value="hub">Cloud Hub</option>
|
||||
<option value="direct">Прямое подключение</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{draft.connectionMode === "hub" && detailsMode === "edit" && editingExecutor ? (
|
||||
<div className="nodedc-ai-settings-download-row">
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Pairing code</span>
|
||||
<input value={editingExecutor.pairingCode || ""} readOnly placeholder="XXXX-XXXX-XXXX" />
|
||||
</label>
|
||||
<a
|
||||
className="modal-btn btn-primary nodedc-ai-settings-download"
|
||||
href={workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, editingExecutor.id)}
|
||||
onClick={downloadAgent}
|
||||
download
|
||||
>
|
||||
<Download className="size-4" />
|
||||
Скачать агент
|
||||
</a>
|
||||
</div>
|
||||
) : draft.connectionMode === "hub" ? (
|
||||
<div className="nodedc-ai-settings-note">
|
||||
После сохранения общий AI Workspace создаст pairing code и installer агента.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.connectionMode === "direct" ? (
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Адрес агента</span>
|
||||
<input value={draft.endpoint} placeholder="http://192.168.1.50:8787" onChange={(event) => setDraft((current) => ({ ...current, endpoint: event.target.value }))} />
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Workspace path</span>
|
||||
<input value={draft.workspacePath} placeholder="C:\\Projects\\NODEDC" onChange={(event) => setDraft((current) => ({ ...current, workspacePath: event.target.value }))} />
|
||||
</label>
|
||||
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Порт агента</span>
|
||||
<input value={draft.agentPort} inputMode="numeric" onChange={(event) => setDraft((current) => ({ ...current, agentPort: event.target.value }))} />
|
||||
</label>
|
||||
|
||||
<label className="nodedc-ai-settings-field">
|
||||
<span>Аккаунт</span>
|
||||
<input value={draft.accountLabel} placeholder="OpenAI account" onChange={(event) => setDraft((current) => ({ ...current, accountLabel: event.target.value }))} />
|
||||
</label>
|
||||
|
||||
<div className="nodedc-ai-settings-field">
|
||||
<span>Capabilities</span>
|
||||
<div className="nodedc-ai-settings-capabilities">
|
||||
{capabilityOptions.map((option) => (
|
||||
<label key={option.value}>
|
||||
<input type="checkbox" checked={draft.capabilities.includes(option.value)} onChange={() => toggleCapability(option.value)} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="nodedc-ai-settings-actions">
|
||||
{detailsMode === "edit" && editingExecutor ? (
|
||||
<button type="button" className="modal-btn nodedc-ai-danger" disabled={isBusy} onClick={deleteEditingExecutor}>
|
||||
<Trash2 className="size-4" />
|
||||
Удалить
|
||||
</button>
|
||||
) : null}
|
||||
<button type="submit" className="modal-btn btn-primary" disabled={isBusy || !draft.name.trim()}>
|
||||
{busyId === "save" ? "Сохранение..." : detailsMode === "new" ? "Подключить" : "Сохранить"}
|
||||
<button
|
||||
type="button"
|
||||
className="ai-console-icon-button"
|
||||
aria-label="Подключить устройство"
|
||||
title="Подключить устройство"
|
||||
onClick={startNew}
|
||||
>
|
||||
<span aria-hidden="true">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{isLoading ? <div className="ai-settings-empty-list">Загрузка</div> : null}
|
||||
{executors.map((executor) => (
|
||||
<button
|
||||
key={executor.id}
|
||||
type="button"
|
||||
className="ai-settings-executor-row"
|
||||
data-active={activeExecutor?.id === executor.id && detailsMode !== "new" ? "true" : undefined}
|
||||
onClick={() => selectExecutor(executor)}
|
||||
>
|
||||
<span className="ai-settings-executor-row__title">
|
||||
<span>{executor.name}</span>
|
||||
<i className="ai-status-dot" data-status={executor.status} aria-hidden="true" />
|
||||
</span>
|
||||
<small>Codex worker · {statusLabel(executor.status)}</small>
|
||||
</button>
|
||||
))}
|
||||
{!isLoading && !executors.length ? (
|
||||
<div className="ai-settings-empty-list">
|
||||
<span>{error ? "Registry недоступен" : "Устройств нет"}</span>
|
||||
<small>
|
||||
{error
|
||||
? String((error as any)?.message || (error as any)?.error || error)
|
||||
: "Подключенные устройства появятся здесь."}
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
|
||||
<section className="ai-settings-context-pane">
|
||||
<div className="ai-settings-form__top">
|
||||
<div>
|
||||
<div className="ai-settings-section-title">
|
||||
{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}
|
||||
</div>
|
||||
<div className="ai-settings-subtitle">Устройство, режим и контекст управления</div>
|
||||
</div>
|
||||
{activeExecutor ? (
|
||||
<span
|
||||
className="ai-executor-status ai-status-dot"
|
||||
data-status={activeExecutor.status}
|
||||
aria-label={statusLabel(activeExecutor.status)}
|
||||
title={statusLabel(activeExecutor.status)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{activeExecutor && detailsMode !== "new" ? (
|
||||
<>
|
||||
<div className="ai-settings-active-model">
|
||||
<div>
|
||||
<span>Активное устройство</span>
|
||||
<b>{activeExecutor.name}</b>
|
||||
<small>Codex worker · Codex Remote</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ai-settings-context-note ai-settings-context-note--remote">
|
||||
Прямое управление Codex-сеансами устройства.
|
||||
</div>
|
||||
|
||||
<div className="modal-actions ai-settings-actions">
|
||||
<button type="button" className="modal-btn" disabled={isBusy} onClick={editActive}>
|
||||
Настройки
|
||||
</button>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={handleClose}>
|
||||
Готово
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : detailsMode === "new" ? (
|
||||
<div className="ai-settings-empty-context">
|
||||
<span>Новое устройство</span>
|
||||
<small>
|
||||
Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer
|
||||
агента.
|
||||
</small>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="ai-settings-empty-context" data-tone="error">
|
||||
<span>Registry backend недоступен</span>
|
||||
<small>{String((error as any)?.message || (error as any)?.error || error)}</small>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={() => void mutate()}>
|
||||
Повторить
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ai-settings-empty-context">
|
||||
<span>Устройство не выбрано</span>
|
||||
<small>
|
||||
Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически.
|
||||
</small>
|
||||
<button type="button" className="modal-btn btn-primary" onClick={startNew}>
|
||||
Подключить устройство
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notice ? (
|
||||
<div className="ai-settings-check-note" data-tone="success">
|
||||
{notice}
|
||||
</div>
|
||||
) : null}
|
||||
{actionError ? <div className="ai-settings-error">{actionError}</div> : null}
|
||||
</section>
|
||||
|
||||
{detailsOpen ? (
|
||||
<form className="ai-settings-form ai-settings-details-pane" onSubmit={saveDraft}>
|
||||
<div className="ai-settings-form__top">
|
||||
<div>
|
||||
<div className="ai-settings-section-title">
|
||||
{detailsMode === "new" ? "Подключить устройство" : "Параметры устройства"}
|
||||
</div>
|
||||
<div className="ai-settings-subtitle">Cloud Hub или прямой endpoint без секретов</div>
|
||||
</div>
|
||||
{detailsMode === "edit" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="ai-settings-mini-close"
|
||||
aria-label="Закрыть параметры устройства"
|
||||
onClick={() => setDetailsMode("context")}
|
||||
>
|
||||
<span className="nodedc-glass-modal__close-mark" aria-hidden="true" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Имя устройства</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={draft.name}
|
||||
placeholder="MACPRO Codex"
|
||||
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Тип</span>
|
||||
<input className="ai-settings-input" value="Codex worker" readOnly />
|
||||
</label>
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Подключение</span>
|
||||
<select
|
||||
className="ai-settings-input"
|
||||
value={draft.connectionMode}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
connectionMode: event.target.value as TAIWorkspaceExecutorConnectionMode,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<option value="hub">Cloud Hub</option>
|
||||
<option value="direct">Прямое подключение</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{draft.connectionMode === "hub" && detailsMode === "edit" && editingExecutor ? (
|
||||
<div className="ai-settings-field-row ai-settings-field-row--download">
|
||||
<label className="ai-settings-field">
|
||||
<span>Pairing code</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={editingExecutor.pairingCode || ""}
|
||||
readOnly
|
||||
placeholder="XXXX-XXXX-XXXX"
|
||||
/>
|
||||
</label>
|
||||
<a
|
||||
className="modal-btn ai-settings-download ai-settings-inline-download"
|
||||
href={workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(
|
||||
workspaceSlug,
|
||||
editingExecutor.id
|
||||
)}
|
||||
onClick={downloadAgent}
|
||||
download
|
||||
>
|
||||
Скачать агент
|
||||
</a>
|
||||
</div>
|
||||
) : draft.connectionMode === "hub" ? (
|
||||
<div className="ai-settings-check-note">
|
||||
После сохранения общий AI Workspace создаст pairing code и installer агента.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.connectionMode === "direct" ? (
|
||||
<label className="ai-settings-field">
|
||||
<span>Адрес агента</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={draft.endpoint}
|
||||
placeholder="http://192.168.1.50:8787"
|
||||
onChange={(event) => setDraft((current) => ({ ...current, endpoint: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Workspace path</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={draft.workspacePath}
|
||||
placeholder="C:\\Projects\\NODEDC"
|
||||
onChange={(event) => setDraft((current) => ({ ...current, workspacePath: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Порт агента</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={draft.agentPort}
|
||||
inputMode="numeric"
|
||||
onChange={(event) => setDraft((current) => ({ ...current, agentPort: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="ai-settings-field">
|
||||
<span>Аккаунт</span>
|
||||
<input
|
||||
className="ai-settings-input"
|
||||
value={draft.accountLabel}
|
||||
placeholder="OpenAI account"
|
||||
onChange={(event) => setDraft((current) => ({ ...current, accountLabel: event.target.value }))}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="ai-settings-field">
|
||||
<span>Capabilities</span>
|
||||
<div className="ai-settings-capabilities">
|
||||
{capabilityOptions.map((option) => (
|
||||
<label key={option.value} className="ai-settings-chip">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.capabilities.includes(option.value)}
|
||||
onChange={() => toggleCapability(option.value)}
|
||||
/>
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-actions ai-settings-actions">
|
||||
{detailsMode === "edit" && editingExecutor ? (
|
||||
<button
|
||||
type="button"
|
||||
className="modal-btn ai-settings-danger"
|
||||
disabled={isBusy}
|
||||
onClick={deleteEditingExecutor}
|
||||
>
|
||||
Удалить
|
||||
</button>
|
||||
) : null}
|
||||
<button type="submit" className="modal-btn btn-primary" disabled={isBusy || !draft.name.trim()}>
|
||||
{busyId === "save" ? "Сохранение..." : detailsMode === "new" ? "Подключить" : "Сохранить"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalCore>
|
||||
|
||||
Reference in New Issue
Block a user