fix: align ops ai workspace settings UI

This commit is contained in:
DCCONSTRUCTIONS 2026-06-09 13:51:48 +03:00
parent 71979526cf
commit 097012ae31
2 changed files with 954 additions and 491 deletions

View File

@ -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,8 +157,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
setActionError("");
}, []);
const runMutation = useCallback(
async (id: string, action: () => Promise<unknown>, successText: string) => {
const runMutation = useCallback(async (id: string, action: () => Promise<unknown>, successText: string) => {
setBusyId(id);
setActionError("");
setNotice("");
@ -166,13 +165,13 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
await action();
setNotice(successText);
} catch (mutationError: any) {
setActionError(String(mutationError?.message || mutationError?.error || mutationError || "AI Workspace request failed"));
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 () => {
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;
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 () => {
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 () => {
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 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, {
window.location.href = workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(
workspaceSlug,
editingExecutor.id,
{
port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT,
});
}, "Installer подготовлен");
}
);
},
"Installer подготовлен"
);
};
return (
@ -287,98 +311,117 @@ 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>
<button type="button" className={aiWorkspaceCloseButtonClassName} onClick={handleClose} aria-label="Закрыть AI Workspace">
<X className="size-5" />
</div>
<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 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-workspace-kicker">Devices</div>
<div className="nodedc-ai-settings-devices-title">Устройства</div>
<div className="ai-console-kicker">AI Space</div>
<div className="ai-settings-devices-title">Устройства</div>
</div>
<button type="button" className="nodedc-ai-icon-button" aria-label="Подключить устройство" onClick={startNew}>
<Plus className="size-4" />
<button
type="button"
className="ai-console-icon-button"
aria-label="Подключить устройство"
title="Подключить устройство"
onClick={startNew}
>
<span aria-hidden="true">+</span>
</button>
</div>
{isLoading ? <div className="nodedc-ai-settings-empty-list">Загрузка</div> : null}
{isLoading ? <div className="ai-settings-empty-list">Загрузка</div> : null}
{executors.map((executor) => (
<button
key={executor.id}
type="button"
className="nodedc-ai-settings-executor-row"
className="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 className="ai-settings-executor-row__title">
<span>{executor.name}</span>
<i className="ai-status-dot" data-status={executor.status} aria-hidden="true" />
</span>
<small>
{executor.connectionMode === "hub" ? executor.pairingCode || "Cloud Hub" : "Direct"} · {statusLabel(executor.status)}
</small>
<small>Codex worker · {statusLabel(executor.status)}</small>
</button>
))}
{!isLoading && !executors.length ? (
<div className="nodedc-ai-settings-empty-list">
<div className="ai-settings-empty-list">
<span>{error ? "Registry недоступен" : "Устройств нет"}</span>
<small>{error ? String((error as any)?.message || (error as any)?.error || error) : "Подключенные устройства появятся здесь."}</small>
<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">
<section className="ai-settings-context-pane">
<div className="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 className="ai-settings-section-title">
{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}
</div>
{activeExecutor ? <span className="nodedc-ai-status-pill">{statusLabel(activeExecutor.status)}</span> : null}
<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="nodedc-ai-settings-active-model">
<div className="ai-settings-active-model">
<div>
<span>Активное устройство</span>
<b>{activeExecutor.name}</b>
<small>{activeExecutor.connectionMode === "hub" ? activeExecutor.pairingCode || "Cloud Hub" : activeExecutor.endpoint}</small>
<small>Codex worker · Codex Remote</small>
</div>
</div>
<div className="nodedc-ai-settings-note" data-ready="true">
Это устройство сохранено в общем AI Workspace registry. Оно будет видно в Engine и Ops для этого же пользователя.
<div className="ai-settings-context-note ai-settings-context-note--remote">
Прямое управление Codex-сеансами устройства.
</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>
<div className="modal-actions ai-settings-actions">
<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">
<div className="ai-settings-empty-context">
<span>Новое устройство</span>
<small>Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer агента.</small>
<small>
Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer
агента.
</small>
</div>
) : error ? (
<div className="nodedc-ai-settings-empty-context" data-tone="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()}>
@ -386,106 +429,173 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
</button>
</div>
) : (
<div className="nodedc-ai-settings-empty-context">
<div className="ai-settings-empty-context">
<span>Устройство не выбрано</span>
<small>Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически.</small>
<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}
{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="nodedc-ai-settings-details-pane" onSubmit={saveDraft}>
<div className="nodedc-ai-settings-form-top">
<form className="ai-settings-form ai-settings-details-pane" onSubmit={saveDraft}>
<div className="ai-settings-form__top">
<div>
<div className="nodedc-ai-settings-section-title">{detailsMode === "new" ? "Подключить устройство" : "Параметры устройства"}</div>
<div className="nodedc-ai-settings-subtitle">Cloud Hub или прямой endpoint</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="nodedc-ai-mini-close" aria-label="Закрыть параметры" onClick={() => setDetailsMode("context")}>
<X className="size-4" />
<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="nodedc-ai-settings-field">
<label className="ai-settings-field">
<span>Имя устройства</span>
<input value={draft.name} placeholder="MACPRO Codex" onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} />
<input
className="ai-settings-input"
value={draft.name}
placeholder="MACPRO Codex"
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
/>
</label>
<label className="nodedc-ai-settings-field">
<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 value={draft.connectionMode} onChange={(event) => setDraft((current) => ({ ...current, connectionMode: event.target.value as TAIWorkspaceExecutorConnectionMode }))}>
<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="nodedc-ai-settings-download-row">
<label className="nodedc-ai-settings-field">
<div className="ai-settings-field-row ai-settings-field-row--download">
<label className="ai-settings-field">
<span>Pairing code</span>
<input value={editingExecutor.pairingCode || ""} readOnly placeholder="XXXX-XXXX-XXXX" />
<input
className="ai-settings-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)}
className="modal-btn ai-settings-download ai-settings-inline-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">
<div className="ai-settings-check-note">
После сохранения общий AI Workspace создаст pairing code и installer агента.
</div>
) : null}
{draft.connectionMode === "direct" ? (
<label className="nodedc-ai-settings-field">
<label className="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 }))} />
<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="nodedc-ai-settings-field">
<label className="ai-settings-field">
<span>Workspace path</span>
<input value={draft.workspacePath} placeholder="C:\\Projects\\NODEDC" onChange={(event) => setDraft((current) => ({ ...current, workspacePath: event.target.value }))} />
<input
className="ai-settings-input"
value={draft.workspacePath}
placeholder="C:\\Projects\\NODEDC"
onChange={(event) => setDraft((current) => ({ ...current, workspacePath: event.target.value }))}
/>
</label>
<label className="nodedc-ai-settings-field">
<label className="ai-settings-field">
<span>Порт агента</span>
<input value={draft.agentPort} inputMode="numeric" onChange={(event) => setDraft((current) => ({ ...current, agentPort: event.target.value }))} />
<input
className="ai-settings-input"
value={draft.agentPort}
inputMode="numeric"
onChange={(event) => setDraft((current) => ({ ...current, agentPort: event.target.value }))}
/>
</label>
<label className="nodedc-ai-settings-field">
<label className="ai-settings-field">
<span>Аккаунт</span>
<input value={draft.accountLabel} placeholder="OpenAI account" onChange={(event) => setDraft((current) => ({ ...current, accountLabel: event.target.value }))} />
<input
className="ai-settings-input"
value={draft.accountLabel}
placeholder="OpenAI account"
onChange={(event) => setDraft((current) => ({ ...current, accountLabel: event.target.value }))}
/>
</label>
<div className="nodedc-ai-settings-field">
<div className="ai-settings-field">
<span>Capabilities</span>
<div className="nodedc-ai-settings-capabilities">
<div className="ai-settings-capabilities">
{capabilityOptions.map((option) => (
<label key={option.value}>
<input type="checkbox" checked={draft.capabilities.includes(option.value)} onChange={() => toggleCapability(option.value)} />
<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="nodedc-ai-settings-actions">
<div className="modal-actions ai-settings-actions">
{detailsMode === "edit" && editingExecutor ? (
<button type="button" className="modal-btn nodedc-ai-danger" disabled={isBusy} onClick={deleteEditingExecutor}>
<Trash2 className="size-4" />
<button
type="button"
className="modal-btn ai-settings-danger"
disabled={isBusy}
onClick={deleteEditingExecutor}
>
Удалить
</button>
) : null}
@ -497,6 +607,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
) : null}
</div>
</div>
</div>
</ModalCore>
</>
);

View File

@ -652,306 +652,658 @@
color: rgb(var(--nodedc-card-active-rgb));
}
.nodedc-ai-workspace-modal {
width: min(74rem, calc(100vw - 2rem));
min-height: min(42rem, calc(100vh - 4rem));
padding: 1.25rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 2rem;
.nodedc-ai-workspace-product-modal {
--wf-share-control-h: 46px;
--brand: rgb(var(--nodedc-accent-rgb));
--brand-rgb: var(--nodedc-accent-rgb);
--brand-contrast: rgb(var(--nodedc-on-accent-rgb));
--nodedc-glass-control-bg: rgba(255, 255, 255, 0.045);
--nodedc-glass-control-hover: rgba(255, 255, 255, 0.075);
--nodedc-glass-control-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 10px 28px rgba(0, 0, 0, 0.2);
width: min(860px, calc(100vw - 32px));
max-width: calc(100vw - 32px);
overflow: visible;
border: 0 !important;
border-radius: 18px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025)),
rgba(22, 23, 26, 0.94);
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(6, 6, 8, 0.92) !important;
padding: 26px;
color: rgba(255, 255, 255, 0.92);
box-shadow: 0 2rem 5rem rgba(0, 0, 0, 0.42);
-webkit-backdrop-filter: blur(34px);
backdrop-filter: blur(34px);
box-shadow:
0 24px 64px rgba(0, 0, 0, 0.42),
0 8px 22px rgba(0, 0, 0, 0.24);
-webkit-backdrop-filter: blur(42px);
backdrop-filter: blur(42px);
}
.nodedc-ai-workspace-modal__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding-right: 3.75rem;
.nodedc-ai-workspace-product-modal:has(.ai-settings-workspace[data-details-open="true"]) {
width: min(1220px, calc(100vw - 32px));
}
.nodedc-ai-workspace-modal__head h3 {
margin: 0;
font-size: 1.15rem;
font-weight: 700;
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__head {
display: block;
margin-bottom: 22px;
padding-right: 54px;
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__titles {
display: grid;
min-width: 0;
gap: 7px;
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__title,
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__accent-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 18px;
font-weight: 760;
letter-spacing: 0;
line-height: 1.18;
}
.nodedc-ai-workspace-kicker {
margin-bottom: 0.25rem;
color: rgb(var(--nodedc-accent-rgb));
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0;
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__title {
color: rgba(255, 255, 255, 0.95);
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__accent-title {
color: rgba(255, 255, 255, 0.68);
text-transform: uppercase;
}
.nodedc-ai-settings-workspace {
display: grid;
grid-template-columns: minmax(14rem, 0.9fr) minmax(18rem, 1.2fr);
gap: 0.75rem;
margin-top: 1rem;
}
.nodedc-ai-settings-workspace[data-details-open="true"] {
grid-template-columns: minmax(13rem, 0.8fr) minmax(17rem, 1fr) minmax(19rem, 1fr);
}
.nodedc-ai-settings-device-list,
.nodedc-ai-settings-context-pane,
.nodedc-ai-settings-details-pane {
min-width: 0;
min-height: 28rem;
padding: 1rem;
border: 1px solid rgba(255, 255, 255, 0.075);
border-radius: 1.55rem;
background: rgba(255, 255, 255, 0.035);
}
.nodedc-ai-settings-details-pane {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.nodedc-ai-settings-devices-head,
.nodedc-ai-settings-form-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.nodedc-ai-settings-devices-title,
.nodedc-ai-settings-section-title {
font-size: 0.95rem;
font-weight: 750;
letter-spacing: 0;
}
.nodedc-ai-settings-subtitle {
margin-top: 0.2rem;
color: rgba(255, 255, 255, 0.48);
font-size: 0.75rem;
line-height: 1.35;
}
.nodedc-ai-icon-button,
.nodedc-ai-mini-close {
display: grid;
width: 2.5rem;
height: 2.5rem;
flex: 0 0 auto;
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close,
.ai-settings-mini-close {
position: absolute;
top: 16px;
right: 16px;
display: inline-grid;
width: 46px;
height: 46px;
place-items: center;
border: 0;
border-radius: 15px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
outline: none !important;
padding: 0;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.ai-settings-mini-close {
position: static;
flex: 0 0 46px;
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close:hover,
.ai-settings-mini-close:hover {
background: var(--nodedc-glass-control-hover);
color: #ffffff;
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close-mark {
position: relative;
display: block;
width: 12px;
height: 12px;
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close-mark::before,
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close-mark::after {
position: absolute;
top: 50%;
left: 50%;
width: 12.5px;
height: 1.2px;
border-radius: 999px;
background: rgba(7, 7, 10, 0.78);
color: rgba(255, 255, 255, 0.8);
background: currentColor;
content: "";
transform-origin: center;
}
.nodedc-ai-settings-executor-row {
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close-mark::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close-mark::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__body {
display: grid;
gap: 16px;
overflow: visible;
}
.ai-settings-workspace {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 14px;
min-height: 472px;
max-height: min(650px, calc(100vh - 190px));
overflow: hidden;
}
.ai-settings-workspace[data-details-open="true"] {
grid-template-columns: 260px minmax(360px, 1fr) minmax(360px, 1fr);
}
.ai-settings-model-list,
.ai-settings-context-pane,
.ai-settings-details-pane {
min-width: 0;
max-height: inherit;
overflow: auto;
border-radius: 22px;
background: rgba(255, 255, 255, 0.025);
box-shadow: var(--nodedc-glass-control-shadow);
}
.ai-settings-model-list::-webkit-scrollbar,
.ai-settings-context-pane::-webkit-scrollbar,
.ai-settings-details-pane::-webkit-scrollbar {
width: 0;
height: 0;
}
.ai-settings-model-list {
display: grid;
align-content: start;
gap: 8px;
padding: 12px;
}
.ai-settings-devices-head {
display: flex;
min-height: var(--wf-share-control-h);
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 0 4px 8px;
}
.ai-console-kicker {
color: rgba(255, 255, 255, 0.48);
font-size: 10px;
font-weight: 760;
letter-spacing: 0;
line-height: 1;
text-transform: uppercase;
}
.ai-settings-devices-title {
margin-top: 7px;
color: rgba(255, 255, 255, 0.94);
font-size: 15px;
font-weight: 760;
line-height: 1.1;
}
.ai-console-icon-button {
display: inline-grid;
width: var(--wf-share-control-h);
height: var(--wf-share-control-h);
flex: 0 0 var(--wf-share-control-h);
place-items: center;
border: 0;
border-radius: 15px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.82);
cursor: pointer;
outline: none !important;
padding: 0;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.ai-console-icon-button:hover {
background: var(--nodedc-glass-control-hover);
color: #ffffff;
}
.ai-console-icon-button > span:not(.nodedc-glass-modal__close-mark) {
font-size: 19px;
font-weight: 560;
line-height: 1;
}
.ai-settings-context-pane {
display: flex;
width: 100%;
min-height: 4rem;
flex-direction: column;
align-items: stretch;
justify-content: center;
gap: 0.3rem;
margin-top: 0.75rem;
padding: 0.8rem;
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 1.1rem;
background: rgba(255, 255, 255, 0.035);
color: inherit;
text-align: left;
gap: 12px;
padding: 14px;
}
.nodedc-ai-settings-executor-row[data-active="true"] {
border-color: rgba(var(--nodedc-accent-rgb), 0.52);
background: rgba(var(--nodedc-accent-rgb), 0.1);
.ai-settings-form {
display: grid;
align-content: start;
gap: 12px;
padding: 14px;
}
.nodedc-ai-settings-executor-row span {
.ai-settings-form__top {
display: flex;
min-width: 0;
align-items: center;
gap: 0.5rem;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.nodedc-ai-settings-executor-row b,
.nodedc-ai-settings-active-model b {
.ai-settings-section-title {
color: rgba(255, 255, 255, 0.92);
font-size: 13px;
font-weight: 760;
line-height: 1.1;
}
.ai-settings-subtitle {
margin-top: 6px;
color: rgba(255, 255, 255, 0.42);
font-size: 11px;
line-height: 1.2;
}
.ai-settings-executor-row {
display: grid;
min-height: 52px;
min-width: 0;
gap: 6px;
border: 0;
border-radius: 14px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.72);
cursor: pointer;
outline: none !important;
padding: 10px 13px;
text-align: left;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.ai-settings-executor-row:hover,
.ai-settings-executor-row[data-active="true"] {
background: var(--nodedc-glass-control-hover);
color: rgba(255, 255, 255, 0.96);
}
.ai-settings-executor-row[data-active="true"] {
background: var(--brand);
color: var(--brand-contrast);
box-shadow: none !important;
}
.ai-settings-executor-row__title,
.ai-settings-executor-row__title span,
.ai-settings-executor-row > span:not(.ai-settings-executor-row__title),
.ai-settings-executor-row small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nodedc-ai-settings-executor-row small,
.nodedc-ai-settings-empty-list small,
.nodedc-ai-settings-empty-context small,
.nodedc-ai-settings-active-model small {
color: rgba(255, 255, 255, 0.5);
font-size: 0.72rem;
.ai-settings-executor-row__title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.ai-settings-executor-row__title span,
.ai-settings-executor-row > span:not(.ai-settings-executor-row__title) {
font-size: 12px;
font-weight: 760;
line-height: 1.1;
}
.ai-settings-executor-row small {
color: rgba(255, 255, 255, 0.42);
font-size: 10px;
line-height: 1;
}
.ai-settings-executor-row[data-active="true"] small {
color: rgba(255, 255, 255, 0.76);
}
.ai-status-dot {
display: inline-block;
width: 10px;
height: 10px;
flex: 0 0 auto;
border-radius: 999px;
background: rgba(255, 255, 255, 0.3);
box-shadow:
0 0 0 3px rgba(255, 255, 255, 0.045),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.ai-status-dot[data-status="online"] {
background: #65d99b;
box-shadow:
0 0 0 3px rgba(101, 217, 155, 0.12),
0 0 18px rgba(101, 217, 155, 0.2);
}
.ai-status-dot[data-status="offline"] {
background: rgba(255, 255, 255, 0.24);
box-shadow:
0 0 0 3px rgba(255, 255, 255, 0.045),
inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.ai-executor-status {
width: 12px;
height: 12px;
margin-top: 4px;
}
.ai-settings-active-model {
display: flex;
min-width: 0;
min-height: var(--wf-share-control-h);
align-items: center;
justify-content: space-between;
gap: 12px;
border-radius: 14px;
background: var(--nodedc-glass-control-bg);
padding: 10px 14px;
box-shadow: var(--nodedc-glass-control-shadow);
}
.ai-settings-active-model > div {
display: grid;
min-width: 0;
gap: 5px;
}
.ai-settings-active-model span,
.ai-settings-active-model b,
.ai-settings-active-model small {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-settings-active-model span {
color: rgba(255, 255, 255, 0.42);
font-size: 10px;
font-weight: 760;
line-height: 1;
text-transform: uppercase;
}
.ai-settings-active-model b {
color: rgba(255, 255, 255, 0.92);
font-size: 13px;
font-weight: 760;
line-height: 1.1;
}
.ai-settings-active-model small {
color: rgba(255, 255, 255, 0.44);
font-size: 11px;
line-height: 1.1;
}
.ai-settings-context-note,
.ai-settings-empty-context {
min-width: 0;
border-radius: 15px;
background: rgba(255, 255, 255, 0.045);
color: rgba(255, 255, 255, 0.52);
padding: 12px;
font-size: 11px;
font-weight: 640;
line-height: 1.35;
}
.nodedc-ai-settings-empty-list,
.nodedc-ai-settings-empty-context,
.nodedc-ai-settings-active-model,
.nodedc-ai-settings-note,
.nodedc-ai-settings-error {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-top: 1rem;
padding: 1rem;
border-radius: 1.25rem;
background: rgba(255, 255, 255, 0.04);
}
.nodedc-ai-settings-empty-context {
min-height: 18rem;
justify-content: center;
text-align: center;
}
.nodedc-ai-settings-empty-context .modal-btn {
margin: 0.7rem auto 0;
}
.nodedc-ai-settings-active-model span {
color: rgba(255, 255, 255, 0.52);
font-size: 0.72rem;
}
.nodedc-ai-settings-note {
color: rgba(255, 255, 255, 0.66);
font-size: 0.75rem;
line-height: 1.4;
}
.nodedc-ai-settings-note[data-ready="true"],
.nodedc-ai-settings-note[data-tone="success"] {
border: 1px solid rgba(var(--nodedc-accent-rgb), 0.18);
background: rgba(var(--nodedc-accent-rgb), 0.08);
}
.nodedc-ai-settings-error,
.nodedc-ai-settings-empty-context[data-tone="error"] {
border: 1px solid rgba(255, 89, 89, 0.22);
background: rgba(255, 89, 89, 0.1);
color: rgb(255, 144, 144);
}
.nodedc-ai-status-pill {
flex: 0 0 auto;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
padding: 0.35rem 0.6rem;
color: rgba(255, 255, 255, 0.62);
font-size: 0.68rem;
font-weight: 700;
}
.nodedc-ai-settings-actions {
.ai-settings-context-note[data-ready="true"] {
background: rgba(80, 210, 145, 0.11);
color: rgba(164, 245, 199, 0.88);
}
.ai-settings-context-note--remote {
background: rgba(255, 255, 255, 0.045);
color: rgba(var(--brand-rgb), 0.96);
}
.ai-settings-empty-context {
display: grid;
align-content: start;
gap: 10px;
}
.ai-settings-empty-context span {
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
font-weight: 760;
}
.ai-settings-empty-context small {
color: rgba(255, 255, 255, 0.48);
font-size: 11px;
line-height: 1.3;
}
.ai-settings-empty-list {
display: grid;
gap: 6px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.035);
color: rgba(255, 255, 255, 0.5);
padding: 11px 13px;
}
.ai-settings-empty-list span {
color: rgba(255, 255, 255, 0.74);
font-size: 12px;
font-weight: 760;
line-height: 1.1;
}
.ai-settings-empty-list small {
color: rgba(255, 255, 255, 0.42);
font-size: 10px;
line-height: 1.25;
}
.ai-settings-field,
.ai-settings-field-row {
min-width: 0;
}
.ai-settings-field {
display: grid;
gap: 7px;
}
.ai-settings-field-row {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 10px;
}
.ai-settings-field-row--download {
align-items: end;
grid-template-columns: minmax(0, 1fr) auto;
}
.ai-settings-field > span {
color: rgba(255, 255, 255, 0.46);
font-size: 10px;
font-weight: 760;
line-height: 1;
text-transform: uppercase;
}
.ai-settings-input {
width: 100%;
height: var(--wf-share-control-h);
min-width: 0;
min-height: var(--wf-share-control-h);
border: 0 !important;
border-radius: 14px;
background: rgba(255, 255, 255, 0.045);
color: rgba(255, 255, 255, 0.92);
outline: none !important;
padding: 0 14px;
font-size: 13px;
font-weight: 680;
box-shadow: var(--nodedc-glass-control-shadow) !important;
}
.ai-settings-input::placeholder {
color: rgba(255, 255, 255, 0.28);
}
.ai-settings-input option {
background: #181820;
color: #ffffff;
}
.ai-settings-capabilities {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 1rem;
gap: 7px;
}
.nodedc-ai-settings-actions .modal-btn,
.nodedc-ai-settings-download {
.ai-settings-chip {
display: inline-flex;
min-height: 2.35rem;
min-height: 32px;
align-items: center;
justify-content: center;
gap: 0.45rem;
gap: 7px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.045);
color: rgba(255, 255, 255, 0.72);
padding: 0 10px;
font-size: 11px;
font-weight: 680;
line-height: 1;
}
.ai-settings-chip input {
appearance: none;
width: 12px;
height: 12px;
margin: 0;
border: 0;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.86);
padding: 0.55rem 0.85rem;
font-size: 0.78rem;
font-weight: 750;
background: rgba(255, 255, 255, 0.22);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
.nodedc-ai-settings-actions .btn-primary,
.nodedc-ai-settings-download.btn-primary {
background: rgb(var(--nodedc-accent-rgb));
color: rgb(var(--nodedc-on-accent-rgb));
.ai-settings-chip input:checked {
background: var(--brand);
box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.16);
}
.nodedc-ai-danger {
color: rgb(255, 144, 144) !important;
.ai-settings-check-note,
.ai-settings-error {
min-width: 0;
overflow-wrap: anywhere;
font-size: 11px;
line-height: 1.35;
}
.nodedc-ai-settings-field {
.ai-settings-check-note {
color: rgba(255, 255, 255, 0.42);
}
.ai-settings-check-note[data-tone="success"] {
color: #88e1b1;
}
.ai-settings-error {
color: #ff9b9b;
}
.modal-actions,
.ai-settings-actions {
display: flex;
flex-direction: column;
gap: 0.35rem;
color: rgba(255, 255, 255, 0.56);
font-size: 0.72rem;
font-weight: 700;
}
.nodedc-ai-settings-field input,
.nodedc-ai-settings-field select {
min-height: 2.55rem;
width: 100%;
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 0.9rem;
background: rgba(7, 7, 10, 0.54);
color: rgba(255, 255, 255, 0.9);
outline: none;
padding: 0 0.8rem;
font-size: 0.82rem;
}
.nodedc-ai-settings-download-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 0.5rem;
}
.nodedc-ai-settings-capabilities {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.nodedc-ai-settings-capabilities label {
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.72);
padding: 0.45rem 0.65rem;
font-size: 0.73rem;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.ai-settings-actions .modal-btn,
.ai-settings-empty-context .modal-btn,
.ai-settings-inline-download {
display: inline-flex;
min-height: var(--wf-share-control-h);
align-items: center;
justify-content: center;
border: 0;
border-radius: 14px;
background: var(--nodedc-glass-control-bg);
color: rgba(255, 255, 255, 0.86);
padding: 0 17px;
font-size: 12px;
font-weight: 680;
line-height: 1.05;
text-decoration: none;
white-space: nowrap;
box-shadow: var(--nodedc-glass-control-shadow);
}
.ai-settings-actions .modal-btn:hover:not(:disabled),
.ai-settings-empty-context .modal-btn:hover:not(:disabled),
.ai-settings-inline-download:hover {
background: var(--nodedc-glass-control-hover);
color: #ffffff;
}
.ai-settings-actions .modal-btn.btn-primary,
.ai-settings-empty-context .modal-btn.btn-primary {
background: var(--brand);
color: var(--brand-contrast);
}
.ai-settings-actions .modal-btn:disabled {
cursor: default;
opacity: 0.48;
}
.ai-settings-context-pane > .ai-settings-actions,
.ai-settings-details-pane > .ai-settings-actions {
margin-top: auto;
}
.ai-settings-details-pane > .ai-settings-actions {
position: sticky;
bottom: 0;
z-index: 2;
padding-top: 8px;
background: transparent;
}
.ai-settings-download {
align-self: auto;
}
.ai-settings-danger {
color: rgba(255, 255, 255, 0.66) !important;
}
@media (max-width: 980px) {
.nodedc-ai-workspace-modal {
.nodedc-ai-workspace-product-modal,
.nodedc-ai-workspace-product-modal:has(.ai-settings-workspace[data-details-open="true"]) {
width: min(640px, calc(100vw - 24px));
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
.nodedc-ai-settings-workspace,
.nodedc-ai-settings-workspace[data-details-open="true"] {
.ai-settings-workspace,
.ai-settings-workspace[data-details-open="true"],
.ai-settings-field-row {
grid-template-columns: 1fr;
}
.nodedc-ai-settings-device-list,
.nodedc-ai-settings-context-pane,
.nodedc-ai-settings-details-pane {
min-height: auto;
.ai-settings-workspace {
min-height: 0;
}
}