fix: align ops ai workspace settings UI
This commit is contained in:
parent
71979526cf
commit
097012ae31
|
|
@ -8,11 +8,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import type { FormEvent, MouseEvent } from "react";
|
import type { FormEvent, MouseEvent } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { CheckCircle2, Download, MessageCircle, Monitor, Plus, RefreshCw, Settings, Trash2, X } from "lucide-react";
|
import { MessageCircle } from "lucide-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// services
|
// services
|
||||||
import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service";
|
import { WorkspaceAIWorkspaceService } from "@/services/workspace-ai-workspace.service";
|
||||||
import type {
|
import type {
|
||||||
|
|
@ -24,14 +23,14 @@ import type {
|
||||||
const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService();
|
const workspaceAIWorkspaceService = new WorkspaceAIWorkspaceService();
|
||||||
const DEFAULT_WINDOWS_AGENT_PORT = "8787";
|
const DEFAULT_WINDOWS_AGENT_PORT = "8787";
|
||||||
const capabilityOptions = [
|
const capabilityOptions = [
|
||||||
|
{ value: "chat", label: "Chat" },
|
||||||
|
{ value: "code", label: "Code" },
|
||||||
{ value: "engine", label: "Engine" },
|
{ value: "engine", label: "Engine" },
|
||||||
{ value: "ops", label: "Ops" },
|
{ 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 = {
|
type TAIWorkspaceGlobalControlProps = {
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
};
|
};
|
||||||
|
|
@ -67,7 +66,8 @@ function draftFromExecutor(executor: TAIWorkspaceExecutor | null | undefined): T
|
||||||
workspacePath: executor.workspacePath || "",
|
workspacePath: executor.workspacePath || "",
|
||||||
agentPort: String(executor.agentPort || DEFAULT_WINDOWS_AGENT_PORT),
|
agentPort: String(executor.agentPort || DEFAULT_WINDOWS_AGENT_PORT),
|
||||||
accountLabel: executor.accountLabel || "",
|
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) {
|
function statusLabel(status: string | null | undefined) {
|
||||||
if (status === "online") return "online";
|
if (status === "online") return "Online";
|
||||||
if (status === "offline") return "offline";
|
if (status === "offline") return "Offline";
|
||||||
if (status === "checking") return "checking";
|
if (status === "checking") return "Checking";
|
||||||
if (status === "error") return "error";
|
if (status === "error") return "Error";
|
||||||
return "unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) {
|
export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalControlProps) {
|
||||||
|
|
@ -157,8 +157,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
setActionError("");
|
setActionError("");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runMutation = useCallback(
|
const runMutation = useCallback(async (id: string, action: () => Promise<unknown>, successText: string) => {
|
||||||
async (id: string, action: () => Promise<unknown>, successText: string) => {
|
|
||||||
setBusyId(id);
|
setBusyId(id);
|
||||||
setActionError("");
|
setActionError("");
|
||||||
setNotice("");
|
setNotice("");
|
||||||
|
|
@ -166,13 +165,13 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
await action();
|
await action();
|
||||||
setNotice(successText);
|
setNotice(successText);
|
||||||
} catch (mutationError: any) {
|
} 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 {
|
} finally {
|
||||||
setBusyId("");
|
setBusyId("");
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const startNew = () => {
|
const startNew = () => {
|
||||||
setDetailsMode("new");
|
setDetailsMode("new");
|
||||||
|
|
@ -215,30 +214,43 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (detailsMode === "new") {
|
if (detailsMode === "new") {
|
||||||
await runMutation("save", async () => {
|
await runMutation(
|
||||||
|
"save",
|
||||||
|
async () => {
|
||||||
const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input);
|
const payload = await workspaceAIWorkspaceService.createExecutor(workspaceSlug, input);
|
||||||
await mutate(payload, { revalidate: false });
|
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 || "");
|
setEditingId(next?.id || "");
|
||||||
setDetailsMode(next ? "edit" : "context");
|
setDetailsMode(next ? "edit" : "context");
|
||||||
}, "Устройство создано");
|
},
|
||||||
|
"Устройство создано"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!editingExecutor) return;
|
if (!editingExecutor) return;
|
||||||
await runMutation("save", async () => {
|
await runMutation(
|
||||||
|
"save",
|
||||||
|
async () => {
|
||||||
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input);
|
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, input);
|
||||||
await mutate(payload, { revalidate: false });
|
await mutate(payload, { revalidate: false });
|
||||||
setDetailsMode("context");
|
setDetailsMode("context");
|
||||||
}, "Устройство сохранено");
|
},
|
||||||
|
"Устройство сохранено"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEditingExecutor = async () => {
|
const deleteEditingExecutor = async () => {
|
||||||
if (!editingExecutor) return;
|
if (!editingExecutor) return;
|
||||||
await runMutation(`delete:${editingExecutor.id}`, async () => {
|
await runMutation(
|
||||||
|
`delete:${editingExecutor.id}`,
|
||||||
|
async () => {
|
||||||
const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id);
|
const payload = await workspaceAIWorkspaceService.deleteExecutor(workspaceSlug, editingExecutor.id);
|
||||||
await mutate(payload, { revalidate: false });
|
await mutate(payload, { revalidate: false });
|
||||||
setDetailsMode("context");
|
setDetailsMode("context");
|
||||||
}, "Устройство удалено");
|
},
|
||||||
|
"Устройство удалено"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCapability = (value: string) => {
|
const toggleCapability = (value: string) => {
|
||||||
|
|
@ -253,13 +265,25 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
const downloadAgent = async (event: MouseEvent<HTMLAnchorElement>) => {
|
const downloadAgent = async (event: MouseEvent<HTMLAnchorElement>) => {
|
||||||
if (!editingExecutor) return;
|
if (!editingExecutor) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await runMutation("download", async () => {
|
await runMutation(
|
||||||
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, editingExecutor.id, toInput(draft));
|
"download",
|
||||||
|
async () => {
|
||||||
|
const payload = await workspaceAIWorkspaceService.updateExecutor(
|
||||||
|
workspaceSlug,
|
||||||
|
editingExecutor.id,
|
||||||
|
toInput(draft)
|
||||||
|
);
|
||||||
await mutate(payload, { revalidate: false });
|
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,
|
port: draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT,
|
||||||
});
|
}
|
||||||
}, "Installer подготовлен");
|
);
|
||||||
|
},
|
||||||
|
"Installer подготовлен"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -287,98 +311,117 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
width={EModalWidth.XXL}
|
width={EModalWidth.XXL}
|
||||||
className="overflow-visible"
|
className="overflow-visible"
|
||||||
>
|
>
|
||||||
<div className="nodedc-ai-workspace-modal relative">
|
<div className="nodedc-glass-modal ai-settings-modal ai-settings-modal--workspace nodedc-ai-workspace-product-modal">
|
||||||
<div className="nodedc-ai-workspace-modal__head">
|
<div className="nodedc-glass-modal__head">
|
||||||
<div>
|
<div className="nodedc-glass-modal__titles">
|
||||||
<div className="nodedc-ai-workspace-kicker">AI Space</div>
|
<div className="nodedc-glass-modal__title">AI Workspace Settings</div>
|
||||||
<h3>AI Workspace</h3>
|
<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">
|
</div>
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-ai-settings-workspace" data-details-open={detailsOpen ? "true" : undefined}>
|
<div className="nodedc-glass-modal__body">
|
||||||
<aside className="nodedc-ai-settings-device-list">
|
<div className="ai-settings-workspace" data-details-open={detailsOpen ? "true" : undefined}>
|
||||||
<div className="nodedc-ai-settings-devices-head">
|
<aside className="ai-settings-model-list">
|
||||||
|
<div className="ai-settings-devices-head">
|
||||||
<div>
|
<div>
|
||||||
<div className="nodedc-ai-workspace-kicker">Devices</div>
|
<div className="ai-console-kicker">AI Space</div>
|
||||||
<div className="nodedc-ai-settings-devices-title">Устройства</div>
|
<div className="ai-settings-devices-title">Устройства</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="nodedc-ai-icon-button" aria-label="Подключить устройство" onClick={startNew}>
|
<button
|
||||||
<Plus className="size-4" />
|
type="button"
|
||||||
|
className="ai-console-icon-button"
|
||||||
|
aria-label="Подключить устройство"
|
||||||
|
title="Подключить устройство"
|
||||||
|
onClick={startNew}
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? <div className="nodedc-ai-settings-empty-list">Загрузка</div> : null}
|
{isLoading ? <div className="ai-settings-empty-list">Загрузка</div> : null}
|
||||||
{executors.map((executor) => (
|
{executors.map((executor) => (
|
||||||
<button
|
<button
|
||||||
key={executor.id}
|
key={executor.id}
|
||||||
type="button"
|
type="button"
|
||||||
className="nodedc-ai-settings-executor-row"
|
className="ai-settings-executor-row"
|
||||||
data-active={activeExecutor?.id === executor.id && detailsMode !== "new" ? "true" : undefined}
|
data-active={activeExecutor?.id === executor.id && detailsMode !== "new" ? "true" : undefined}
|
||||||
onClick={() => selectExecutor(executor)}
|
onClick={() => selectExecutor(executor)}
|
||||||
>
|
>
|
||||||
<span>
|
<span className="ai-settings-executor-row__title">
|
||||||
<Monitor className="size-4" />
|
<span>{executor.name}</span>
|
||||||
<b>{executor.name}</b>
|
<i className="ai-status-dot" data-status={executor.status} aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
<small>
|
<small>Codex worker · {statusLabel(executor.status)}</small>
|
||||||
{executor.connectionMode === "hub" ? executor.pairingCode || "Cloud Hub" : "Direct"} · {statusLabel(executor.status)}
|
|
||||||
</small>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{!isLoading && !executors.length ? (
|
{!isLoading && !executors.length ? (
|
||||||
<div className="nodedc-ai-settings-empty-list">
|
<div className="ai-settings-empty-list">
|
||||||
<span>{error ? "Registry недоступен" : "Устройств нет"}</span>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="nodedc-ai-settings-context-pane">
|
<section className="ai-settings-context-pane">
|
||||||
<div className="nodedc-ai-settings-form-top">
|
<div className="ai-settings-form__top">
|
||||||
<div>
|
<div>
|
||||||
<div className="nodedc-ai-settings-section-title">{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}</div>
|
<div className="ai-settings-section-title">
|
||||||
<div className="nodedc-ai-settings-subtitle">Общий пользовательский ассистент для Ops и Engine</div>
|
{detailsMode === "new" ? "Новое устройство" : "Рабочий контекст"}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{activeExecutor && detailsMode !== "new" ? (
|
{activeExecutor && detailsMode !== "new" ? (
|
||||||
<>
|
<>
|
||||||
<div className="nodedc-ai-settings-active-model">
|
<div className="ai-settings-active-model">
|
||||||
|
<div>
|
||||||
<span>Активное устройство</span>
|
<span>Активное устройство</span>
|
||||||
<b>{activeExecutor.name}</b>
|
<b>{activeExecutor.name}</b>
|
||||||
<small>{activeExecutor.connectionMode === "hub" ? activeExecutor.pairingCode || "Cloud Hub" : activeExecutor.endpoint}</small>
|
<small>Codex worker · Codex Remote</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-ai-settings-note" data-ready="true">
|
<div className="ai-settings-context-note ai-settings-context-note--remote">
|
||||||
Это устройство сохранено в общем AI Workspace registry. Оно будет видно в Engine и Ops для этого же пользователя.
|
Прямое управление Codex-сеансами устройства.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-ai-settings-actions">
|
<div className="modal-actions 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}>
|
<button type="button" className="modal-btn" disabled={isBusy} onClick={editActive}>
|
||||||
<Settings className="size-4" />
|
|
||||||
Настройки
|
Настройки
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="modal-btn btn-primary" onClick={handleClose}>
|
<button type="button" className="modal-btn btn-primary" onClick={handleClose}>
|
||||||
<CheckCircle2 className="size-4" />
|
|
||||||
Готово
|
Готово
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : detailsMode === "new" ? (
|
) : detailsMode === "new" ? (
|
||||||
<div className="nodedc-ai-settings-empty-context">
|
<div className="ai-settings-empty-context">
|
||||||
<span>Новое устройство</span>
|
<span>Новое устройство</span>
|
||||||
<small>Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer агента.</small>
|
<small>
|
||||||
|
Заполни параметры справа и нажми «Подключить». После сохранения появятся pairing code и installer
|
||||||
|
агента.
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="nodedc-ai-settings-empty-context" data-tone="error">
|
<div className="ai-settings-empty-context" data-tone="error">
|
||||||
<span>Registry backend недоступен</span>
|
<span>Registry backend недоступен</span>
|
||||||
<small>{String((error as any)?.message || (error as any)?.error || error)}</small>
|
<small>{String((error as any)?.message || (error as any)?.error || error)}</small>
|
||||||
<button type="button" className="modal-btn btn-primary" onClick={() => void mutate()}>
|
<button type="button" className="modal-btn btn-primary" onClick={() => void mutate()}>
|
||||||
|
|
@ -386,106 +429,173 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="nodedc-ai-settings-empty-context">
|
<div className="ai-settings-empty-context">
|
||||||
<span>Устройство не выбрано</span>
|
<span>Устройство не выбрано</span>
|
||||||
<small>Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически.</small>
|
<small>
|
||||||
|
Подключи Codex worker. Если устройство уже добавлено в Engine, оно появится здесь автоматически.
|
||||||
|
</small>
|
||||||
<button type="button" className="modal-btn btn-primary" onClick={startNew}>
|
<button type="button" className="modal-btn btn-primary" onClick={startNew}>
|
||||||
Подключить устройство
|
Подключить устройство
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{notice ? <div className="nodedc-ai-settings-note" data-tone="success">{notice}</div> : null}
|
{notice ? (
|
||||||
{actionError ? <div className="nodedc-ai-settings-error">{actionError}</div> : null}
|
<div className="ai-settings-check-note" data-tone="success">
|
||||||
|
{notice}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{actionError ? <div className="ai-settings-error">{actionError}</div> : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{detailsOpen ? (
|
{detailsOpen ? (
|
||||||
<form className="nodedc-ai-settings-details-pane" onSubmit={saveDraft}>
|
<form className="ai-settings-form ai-settings-details-pane" onSubmit={saveDraft}>
|
||||||
<div className="nodedc-ai-settings-form-top">
|
<div className="ai-settings-form__top">
|
||||||
<div>
|
<div>
|
||||||
<div className="nodedc-ai-settings-section-title">{detailsMode === "new" ? "Подключить устройство" : "Параметры устройства"}</div>
|
<div className="ai-settings-section-title">
|
||||||
<div className="nodedc-ai-settings-subtitle">Cloud Hub или прямой endpoint</div>
|
{detailsMode === "new" ? "Подключить устройство" : "Параметры устройства"}
|
||||||
|
</div>
|
||||||
|
<div className="ai-settings-subtitle">Cloud Hub или прямой endpoint без секретов</div>
|
||||||
</div>
|
</div>
|
||||||
{detailsMode === "edit" ? (
|
{detailsMode === "edit" ? (
|
||||||
<button type="button" className="nodedc-ai-mini-close" aria-label="Закрыть параметры" onClick={() => setDetailsMode("context")}>
|
<button
|
||||||
<X className="size-4" />
|
type="button"
|
||||||
|
className="ai-settings-mini-close"
|
||||||
|
aria-label="Закрыть параметры устройства"
|
||||||
|
onClick={() => setDetailsMode("context")}
|
||||||
|
>
|
||||||
|
<span className="nodedc-glass-modal__close-mark" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Имя устройства</span>
|
<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>
|
||||||
|
|
||||||
<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>
|
<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="hub">Cloud Hub</option>
|
||||||
<option value="direct">Прямое подключение</option>
|
<option value="direct">Прямое подключение</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{draft.connectionMode === "hub" && detailsMode === "edit" && editingExecutor ? (
|
{draft.connectionMode === "hub" && detailsMode === "edit" && editingExecutor ? (
|
||||||
<div className="nodedc-ai-settings-download-row">
|
<div className="ai-settings-field-row ai-settings-field-row--download">
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Pairing code</span>
|
<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>
|
</label>
|
||||||
<a
|
<a
|
||||||
className="modal-btn btn-primary nodedc-ai-settings-download"
|
className="modal-btn ai-settings-download ai-settings-inline-download"
|
||||||
href={workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, editingExecutor.id)}
|
href={workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(
|
||||||
|
workspaceSlug,
|
||||||
|
editingExecutor.id
|
||||||
|
)}
|
||||||
onClick={downloadAgent}
|
onClick={downloadAgent}
|
||||||
download
|
download
|
||||||
>
|
>
|
||||||
<Download className="size-4" />
|
|
||||||
Скачать агент
|
Скачать агент
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : draft.connectionMode === "hub" ? (
|
) : draft.connectionMode === "hub" ? (
|
||||||
<div className="nodedc-ai-settings-note">
|
<div className="ai-settings-check-note">
|
||||||
После сохранения общий AI Workspace создаст pairing code и installer агента.
|
После сохранения общий AI Workspace создаст pairing code и installer агента.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{draft.connectionMode === "direct" ? (
|
{draft.connectionMode === "direct" ? (
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Адрес агента</span>
|
<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>
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Workspace path</span>
|
<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>
|
||||||
|
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Порт агента</span>
|
<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>
|
||||||
|
|
||||||
<label className="nodedc-ai-settings-field">
|
<label className="ai-settings-field">
|
||||||
<span>Аккаунт</span>
|
<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>
|
</label>
|
||||||
|
|
||||||
<div className="nodedc-ai-settings-field">
|
<div className="ai-settings-field">
|
||||||
<span>Capabilities</span>
|
<span>Capabilities</span>
|
||||||
<div className="nodedc-ai-settings-capabilities">
|
<div className="ai-settings-capabilities">
|
||||||
{capabilityOptions.map((option) => (
|
{capabilityOptions.map((option) => (
|
||||||
<label key={option.value}>
|
<label key={option.value} className="ai-settings-chip">
|
||||||
<input type="checkbox" checked={draft.capabilities.includes(option.value)} onChange={() => toggleCapability(option.value)} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={draft.capabilities.includes(option.value)}
|
||||||
|
onChange={() => toggleCapability(option.value)}
|
||||||
|
/>
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="nodedc-ai-settings-actions">
|
<div className="modal-actions ai-settings-actions">
|
||||||
{detailsMode === "edit" && editingExecutor ? (
|
{detailsMode === "edit" && editingExecutor ? (
|
||||||
<button type="button" className="modal-btn nodedc-ai-danger" disabled={isBusy} onClick={deleteEditingExecutor}>
|
<button
|
||||||
<Trash2 className="size-4" />
|
type="button"
|
||||||
|
className="modal-btn ai-settings-danger"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={deleteEditingExecutor}
|
||||||
|
>
|
||||||
Удалить
|
Удалить
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -497,6 +607,7 @@ export function AIWorkspaceGlobalControl({ workspaceSlug }: TAIWorkspaceGlobalCo
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</ModalCore>
|
</ModalCore>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -652,306 +652,658 @@
|
||||||
color: rgb(var(--nodedc-card-active-rgb));
|
color: rgb(var(--nodedc-card-active-rgb));
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-workspace-modal {
|
.nodedc-ai-workspace-product-modal {
|
||||||
width: min(74rem, calc(100vw - 2rem));
|
--wf-share-control-h: 46px;
|
||||||
min-height: min(42rem, calc(100vh - 4rem));
|
--brand: rgb(var(--nodedc-accent-rgb));
|
||||||
padding: 1.25rem;
|
--brand-rgb: var(--nodedc-accent-rgb);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
--brand-contrast: rgb(var(--nodedc-on-accent-rgb));
|
||||||
border-radius: 2rem;
|
--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:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025)),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.036) 0%, rgba(255, 255, 255, 0.012) 100%), rgba(6, 6, 8, 0.92) !important;
|
||||||
rgba(22, 23, 26, 0.94);
|
padding: 26px;
|
||||||
color: rgba(255, 255, 255, 0.92);
|
color: rgba(255, 255, 255, 0.92);
|
||||||
box-shadow: 0 2rem 5rem rgba(0, 0, 0, 0.42);
|
box-shadow:
|
||||||
-webkit-backdrop-filter: blur(34px);
|
0 24px 64px rgba(0, 0, 0, 0.42),
|
||||||
backdrop-filter: blur(34px);
|
0 8px 22px rgba(0, 0, 0, 0.24);
|
||||||
|
-webkit-backdrop-filter: blur(42px);
|
||||||
|
backdrop-filter: blur(42px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-workspace-modal__head {
|
.nodedc-ai-workspace-product-modal:has(.ai-settings-workspace[data-details-open="true"]) {
|
||||||
display: flex;
|
width: min(1220px, calc(100vw - 32px));
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding-right: 3.75rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-workspace-modal__head h3 {
|
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__head {
|
||||||
margin: 0;
|
display: block;
|
||||||
font-size: 1.15rem;
|
margin-bottom: 22px;
|
||||||
font-weight: 700;
|
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;
|
letter-spacing: 0;
|
||||||
|
line-height: 1.18;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-workspace-kicker {
|
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__title {
|
||||||
margin-bottom: 0.25rem;
|
color: rgba(255, 255, 255, 0.95);
|
||||||
color: rgb(var(--nodedc-accent-rgb));
|
}
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 800;
|
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__accent-title {
|
||||||
letter-spacing: 0;
|
color: rgba(255, 255, 255, 0.68);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-workspace {
|
.nodedc-ai-workspace-product-modal .nodedc-glass-modal__close,
|
||||||
display: grid;
|
.ai-settings-mini-close {
|
||||||
grid-template-columns: minmax(14rem, 0.9fr) minmax(18rem, 1.2fr);
|
position: absolute;
|
||||||
gap: 0.75rem;
|
top: 16px;
|
||||||
margin-top: 1rem;
|
right: 16px;
|
||||||
}
|
display: inline-grid;
|
||||||
|
width: 46px;
|
||||||
.nodedc-ai-settings-workspace[data-details-open="true"] {
|
height: 46px;
|
||||||
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;
|
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 0;
|
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;
|
border-radius: 999px;
|
||||||
background: rgba(7, 7, 10, 0.78);
|
background: currentColor;
|
||||||
color: rgba(255, 255, 255, 0.8);
|
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;
|
display: flex;
|
||||||
width: 100%;
|
|
||||||
min-height: 4rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
gap: 12px;
|
||||||
justify-content: center;
|
padding: 14px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-executor-row[data-active="true"] {
|
.ai-settings-form {
|
||||||
border-color: rgba(var(--nodedc-accent-rgb), 0.52);
|
display: grid;
|
||||||
background: rgba(var(--nodedc-accent-rgb), 0.1);
|
align-content: start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-executor-row span {
|
.ai-settings-form__top {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
gap: 0.5rem;
|
justify-content: space-between;
|
||||||
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-executor-row b,
|
.ai-settings-section-title {
|
||||||
.nodedc-ai-settings-active-model b {
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-executor-row small,
|
.ai-settings-executor-row__title {
|
||||||
.nodedc-ai-settings-empty-list small,
|
display: flex;
|
||||||
.nodedc-ai-settings-empty-context small,
|
align-items: center;
|
||||||
.nodedc-ai-settings-active-model small {
|
justify-content: space-between;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
gap: 8px;
|
||||||
font-size: 0.72rem;
|
}
|
||||||
|
|
||||||
|
.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;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-empty-list,
|
.ai-settings-context-note[data-ready="true"] {
|
||||||
.nodedc-ai-settings-empty-context,
|
background: rgba(80, 210, 145, 0.11);
|
||||||
.nodedc-ai-settings-active-model,
|
color: rgba(164, 245, 199, 0.88);
|
||||||
.nodedc-ai-settings-note,
|
}
|
||||||
.nodedc-ai-settings-error {
|
|
||||||
display: flex;
|
.ai-settings-context-note--remote {
|
||||||
flex-direction: column;
|
background: rgba(255, 255, 255, 0.045);
|
||||||
gap: 0.35rem;
|
color: rgba(var(--brand-rgb), 0.96);
|
||||||
margin-top: 1rem;
|
}
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 1.25rem;
|
.ai-settings-empty-context {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
display: grid;
|
||||||
}
|
align-content: start;
|
||||||
|
gap: 10px;
|
||||||
.nodedc-ai-settings-empty-context {
|
}
|
||||||
min-height: 18rem;
|
|
||||||
justify-content: center;
|
.ai-settings-empty-context span {
|
||||||
text-align: center;
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
font-size: 13px;
|
||||||
|
font-weight: 760;
|
||||||
.nodedc-ai-settings-empty-context .modal-btn {
|
}
|
||||||
margin: 0.7rem auto 0;
|
|
||||||
}
|
.ai-settings-empty-context small {
|
||||||
|
color: rgba(255, 255, 255, 0.48);
|
||||||
.nodedc-ai-settings-active-model span {
|
font-size: 11px;
|
||||||
color: rgba(255, 255, 255, 0.52);
|
line-height: 1.3;
|
||||||
font-size: 0.72rem;
|
}
|
||||||
}
|
|
||||||
|
.ai-settings-empty-list {
|
||||||
.nodedc-ai-settings-note {
|
display: grid;
|
||||||
color: rgba(255, 255, 255, 0.66);
|
gap: 6px;
|
||||||
font-size: 0.75rem;
|
border-radius: 14px;
|
||||||
line-height: 1.4;
|
background: rgba(255, 255, 255, 0.035);
|
||||||
}
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
padding: 11px 13px;
|
||||||
.nodedc-ai-settings-note[data-ready="true"],
|
}
|
||||||
.nodedc-ai-settings-note[data-tone="success"] {
|
|
||||||
border: 1px solid rgba(var(--nodedc-accent-rgb), 0.18);
|
.ai-settings-empty-list span {
|
||||||
background: rgba(var(--nodedc-accent-rgb), 0.08);
|
color: rgba(255, 255, 255, 0.74);
|
||||||
}
|
font-size: 12px;
|
||||||
|
font-weight: 760;
|
||||||
.nodedc-ai-settings-error,
|
line-height: 1.1;
|
||||||
.nodedc-ai-settings-empty-context[data-tone="error"] {
|
}
|
||||||
border: 1px solid rgba(255, 89, 89, 0.22);
|
|
||||||
background: rgba(255, 89, 89, 0.1);
|
.ai-settings-empty-list small {
|
||||||
color: rgb(255, 144, 144);
|
color: rgba(255, 255, 255, 0.42);
|
||||||
}
|
font-size: 10px;
|
||||||
|
line-height: 1.25;
|
||||||
.nodedc-ai-status-pill {
|
}
|
||||||
flex: 0 0 auto;
|
|
||||||
border-radius: 999px;
|
.ai-settings-field,
|
||||||
background: rgba(255, 255, 255, 0.06);
|
.ai-settings-field-row {
|
||||||
padding: 0.35rem 0.6rem;
|
min-width: 0;
|
||||||
color: rgba(255, 255, 255, 0.62);
|
}
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
.ai-settings-field {
|
||||||
}
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
.nodedc-ai-settings-actions {
|
}
|
||||||
|
|
||||||
|
.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;
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
gap: 7px;
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-actions .modal-btn,
|
.ai-settings-chip {
|
||||||
.nodedc-ai-settings-download {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-height: 2.35rem;
|
min-height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 7px;
|
||||||
gap: 0.45rem;
|
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: 0;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.22);
|
||||||
color: rgba(255, 255, 255, 0.86);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
padding: 0.55rem 0.85rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 750;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-actions .btn-primary,
|
.ai-settings-chip input:checked {
|
||||||
.nodedc-ai-settings-download.btn-primary {
|
background: var(--brand);
|
||||||
background: rgb(var(--nodedc-accent-rgb));
|
box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.16);
|
||||||
color: rgb(var(--nodedc-on-accent-rgb));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-danger {
|
.ai-settings-check-note,
|
||||||
color: rgb(255, 144, 144) !important;
|
.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;
|
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;
|
align-items: center;
|
||||||
gap: 0.35rem;
|
justify-content: flex-end;
|
||||||
border-radius: 999px;
|
gap: 10px;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
flex-wrap: wrap;
|
||||||
color: rgba(255, 255, 255, 0.72);
|
}
|
||||||
padding: 0.45rem 0.65rem;
|
|
||||||
font-size: 0.73rem;
|
.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) {
|
@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);
|
max-height: calc(100vh - 2rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-workspace,
|
.ai-settings-workspace,
|
||||||
.nodedc-ai-settings-workspace[data-details-open="true"] {
|
.ai-settings-workspace[data-details-open="true"],
|
||||||
|
.ai-settings-field-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodedc-ai-settings-device-list,
|
.ai-settings-workspace {
|
||||||
.nodedc-ai-settings-context-pane,
|
min-height: 0;
|
||||||
.nodedc-ai-settings-details-pane {
|
|
||||||
min-height: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue