sync: UI animations, select styling, TLS verify flag via proxy second line, brand spacing
This commit is contained in:
@@ -15,31 +15,38 @@ html, body, button, input, select, textarea, code, pre, a, .chip-btn, .group-tit
|
||||
--node: #0e1116;
|
||||
--node-border: #334155;
|
||||
--node-selected: #1f2937;
|
||||
|
||||
/* Базовый цвет проводов по умолчанию */
|
||||
--connector: #7aa2f7;
|
||||
--connector-muted: #3b82f6;
|
||||
|
||||
/* Неброские цвета для разных типов/веток */
|
||||
--wire-true: #34d399; /* мягкий зелёный для If:true */
|
||||
--wire-false: #94a3b8; /* сланцево‑серый для If:false */
|
||||
--wire-provider: #5b86e5; /* приглушённый синий */
|
||||
--wire-raw: #8b7de6; /* мягкий фиолетовый */
|
||||
--wire-setvars: #4fbfa0; /* приглушённая мята */
|
||||
--wire-return: #93a9d1; /* холодный серо‑синий */
|
||||
|
||||
/* DRY tokens: unified shadows and transitions */
|
||||
--ring3-22-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35);
|
||||
--ring3-20-shadow: 0 0 0 3px rgba(96,165,250,.20), 0 4px 10px rgba(0,0,0,.35);
|
||||
--ring2-20-shadow: 0 0 0 2px rgba(96,165,250,.20), 0 2px 6px rgba(0,0,0,.35);
|
||||
--focus-ring3-20: 0 0 0 3px rgba(96,165,250,.20);
|
||||
--focus-ring3-22: 0 0 0 3px rgba(96,165,250,.22);
|
||||
--tr-base: border-color .12s ease, box-shadow .12s ease, background-color .12s ease, color .12s ease;
|
||||
--tr-pop: transform .12s ease;
|
||||
--tr-pop-fast: transform .08s ease;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; /* убираем общие скролл-бары страницы, чтобы не перекрывать правую стрелку */
|
||||
}
|
||||
|
||||
/* Глобальные контейнеры и скроллы */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; /* убираем общие скролл-бары страницы */
|
||||
}
|
||||
#container {
|
||||
position: relative; /* якорь для абсолютных стрелок-переключателей */
|
||||
}
|
||||
|
||||
/* Глобальные контейнеры и скроллы */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow: hidden; /* убираем общие скролл-бары страницы */
|
||||
}
|
||||
#container {
|
||||
position: relative; /* якорь для абсолютных стрелок-переключателей */
|
||||
}
|
||||
/* Grid areas to hard-pin layout regardless of hidden panels or absolute children */
|
||||
#container {
|
||||
display: grid;
|
||||
@@ -77,7 +84,36 @@ html, body {
|
||||
border: 1px solid var(--node-border);
|
||||
color: #e5e7eb;
|
||||
border-radius: 12px 12px 0 0;
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px; /* компактнее заголовок */
|
||||
font-size: 12px; /* компактнее шрифт заголовка */
|
||||
line-height: 1.2;
|
||||
}
|
||||
/* Иконка типа ноды в заголовке (монохромная, спокойная) */
|
||||
.drawflow .drawflow-node .title-box .node-ico {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 6px;
|
||||
vertical-align: -2px;
|
||||
background-size: 14px 14px;
|
||||
background-repeat: no-repeat;
|
||||
filter: opacity(.9);
|
||||
}
|
||||
/* SVG-иконки по типам (цвета под стиль проекта) */
|
||||
.drawflow .drawflow-node .title-box .node-ico-If {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 4v6a4 4 0 0 0 4 4h4'/><polyline points='14 14 18 10 14 6'/></svg>");
|
||||
}
|
||||
.drawflow .drawflow-node .title-box .node-ico-ProviderCall {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%235b86e5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M3 15a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4'/><path d='M7 19V5a4 4 0 0 1 4-4h2a4 4 0 0 1 4 4v14'/></svg>");
|
||||
}
|
||||
.drawflow .drawflow-node .title-box .node-ico-RawForward {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238b7de6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='5 12 19 12'/><polyline points='12 5 19 12 12 19'/></svg>");
|
||||
}
|
||||
.drawflow .drawflow-node .title-box .node-ico-SetVars {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%234fbfa0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='3'/><path d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.65 1.65 0 0 0 15 19.4a1.65 1.65 0 0 0-1 .6l-.09.1a2 2 0 1 1-3.2 0l-.09-.1a1.65 1.65 0 0 0-1-.6 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-.6-1l-.1-.09a2 2 0 1 1 0-3.2l.1-.09a1.65 1.65 0 0 0 .6-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 6.94 2.6l.06.06A1.65 1.65 0 0 0 8 3.6a1.65 1.65 0 0 0 1-.6l.09-.1a2 2 0 1 1 3.2 0l.09.1a1.65 1.65 0 0 0 1 .6 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 .6 1l.1.09a2 2 0 1 1 0 3.2l-.1.09a1.65 1.65 0 0 0-.6 1z'/></svg>");
|
||||
}
|
||||
.drawflow .drawflow-node .title-box .node-ico-Return {
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2393a9d1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M9 10l-5 5 5 5'/><path d='M20 4v7a4 4 0 0 1-4 4H4'/></svg>");
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node .box {
|
||||
@@ -87,11 +123,28 @@ html, body {
|
||||
color: #e5e7eb;
|
||||
border-radius: 0 0 12px 12px;
|
||||
overflow: hidden; /* не даём контенту вылезать за края */
|
||||
font-size: 11px; /* компактнее содержимое */
|
||||
line-height: 1.25;
|
||||
}
|
||||
/* Контент превью внутри .box: можем скрывать его в LOD, не ломая геометрию ноды */
|
||||
/* Контент превью внутри .box: можем скрывать его в LOD, не меняя коробку ноды */
|
||||
.drawflow .drawflow-node .node-preview {
|
||||
pointer-events: none;
|
||||
pointer-events: auto; /* разрешаем клики по summary (<details>) */
|
||||
opacity: .85;
|
||||
font-size: 10.5px; /* мелкий общий текст превью */
|
||||
}
|
||||
/* На самом канвасе поля превью недоступны для редактирования/клика */
|
||||
.drawflow .drawflow-node .node-preview input,
|
||||
.drawflow .drawflow-node .node-preview textarea {
|
||||
pointer-events: none;
|
||||
}
|
||||
.drawflow .drawflow-node .node-preview label {
|
||||
font-size: 10px;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
/* Адресные поля читаемые «обычным» кеглем */
|
||||
.drawflow .drawflow-node .node-preview .np-url,
|
||||
.drawflow .drawflow-node .node-preview .np-endpoint {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node .box textarea,
|
||||
@@ -104,6 +157,9 @@ html, body {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px; /* компактнее поля превью */
|
||||
font-size: 10.5px; /* мелкий текст по умолчанию */
|
||||
resize: none; /* запрет изменения размера на канвасе */
|
||||
}
|
||||
|
||||
.df-node .box textarea {
|
||||
@@ -112,6 +168,7 @@ html, body {
|
||||
overflow-y: auto; /* только вертикальный скролл при необходимости */
|
||||
overflow-x: hidden; /* убираем горизонтальный скролл внутри textarea */
|
||||
max-height: 180px; /* предотвращаем бесконечную высоту */
|
||||
resize: none; /* запрет ручного ресайза превью */
|
||||
}
|
||||
|
||||
/* Выделение выбранного узла — мягкое */
|
||||
@@ -120,6 +177,15 @@ html, body {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
}
|
||||
/* Привести disabled к виду обычных превью (без «серости» браузера) */
|
||||
.drawflow .drawflow-node .box input[disabled],
|
||||
.drawflow .drawflow-node .box textarea[disabled] {
|
||||
opacity: 1;
|
||||
color: #e5e7eb;
|
||||
background: #0f141a;
|
||||
border-color: #2b3646;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Порты: более аккуратные, без «оранжевого» */
|
||||
.drawflow .drawflow-node .inputs .input,
|
||||
@@ -131,19 +197,36 @@ html, body {
|
||||
box-shadow: 0 0 0 2px rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
/* Линии соединений: плавные, аккуратные цвета */
|
||||
/* Линии соединений: тоньше и спокойнее */
|
||||
.drawflow .connection .main-path {
|
||||
stroke: var(--connector) !important;
|
||||
/* Толщина линии масштабируется от зума (var(--zoom) задаётся на #canvas из JS) */
|
||||
stroke-width: clamp(1.6px, calc(3px / var(--zoom, 1)), 6px) !important;
|
||||
opacity: 0.95 !important;
|
||||
stroke-width: clamp(1px, calc(2.2px / var(--zoom, 1)), 4.5px) !important;
|
||||
opacity: 0.9 !important;
|
||||
stroke-linecap: round; /* сглаженные окончания */
|
||||
stroke-linejoin: round; /* сглажённые соединения */
|
||||
}
|
||||
|
||||
/* Connection styling classes (set by JS; stable even if Drawflow re-renders paths) */
|
||||
.drawflow .connection.conn-if-true .main-path {
|
||||
stroke: var(--wire-true) !important;
|
||||
stroke-dasharray: 6 6 !important;
|
||||
}
|
||||
.drawflow .connection.conn-if-false .main-path {
|
||||
stroke: var(--wire-false) !important;
|
||||
stroke-dasharray: 6 6 !important;
|
||||
}
|
||||
.drawflow .connection.conn-provider .main-path { stroke: var(--wire-provider) !important; }
|
||||
.drawflow .connection.conn-raw .main-path { stroke: var(--wire-raw) !important; }
|
||||
.drawflow .connection.conn-setvars .main-path { stroke: var(--wire-setvars) !important; }
|
||||
.drawflow .connection.conn-return .main-path { stroke: var(--wire-return) !important; }
|
||||
/* Подсветка входящих к ошибочной ноде рёбер (мягкий красный) */
|
||||
.drawflow .connection.conn-upstream-err .main-path { stroke: #ef4444 !important; opacity: .95 !important; }
|
||||
|
||||
.drawflow .connection .main-path.selected,
|
||||
.drawflow .connection:hover .main-path {
|
||||
stroke: var(--accent-2) !important;
|
||||
/* На hover/selected — немного толще базовой формулы */
|
||||
stroke-width: clamp(2px, calc(3.6px / var(--zoom, 1)), 7px) !important;
|
||||
/* На hover/selected — слегка толще базовой формулы */
|
||||
stroke-width: clamp(1.3px, calc(2.6px / var(--zoom, 1)), 5px) !important;
|
||||
}
|
||||
|
||||
/* Точки изгибов/ручки */
|
||||
@@ -268,19 +351,19 @@ a.chip-btn {
|
||||
color: #e5e7eb;
|
||||
border: 1px solid #334155;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.35);
|
||||
transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease, border-color .12s ease, color .12s ease;
|
||||
transition: var(--tr-base), var(--tr-pop);
|
||||
user-select: none;
|
||||
}
|
||||
.chip-btn:hover,
|
||||
a.chip-btn:hover {
|
||||
background: #1f2937;
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35);
|
||||
box-shadow: var(--ring3-22-shadow);
|
||||
}
|
||||
.chip-btn:active,
|
||||
a.chip-btn:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 0 0 2px rgba(96,165,250,.20), 0 2px 6px rgba(0,0,0,.35);
|
||||
box-shadow: var(--ring2-20-shadow);
|
||||
}
|
||||
|
||||
/* Инпуты и селекты в шапке — в одном визуальном ряду с чипами */
|
||||
@@ -297,7 +380,7 @@ a.chip-btn:active {
|
||||
}
|
||||
.top-input:focus {
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.20);
|
||||
box-shadow: var(--focus-ring3-20);
|
||||
}
|
||||
|
||||
/* Внутренние заголовки в блоке ноды */
|
||||
@@ -322,7 +405,7 @@ a.chip-btn:active {
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.35);
|
||||
cursor: pointer;
|
||||
opacity: .85;
|
||||
transition: transform .12s ease, opacity .12s ease, box-shadow .12s ease, border-color .12s ease, background-color .12s ease;
|
||||
transition: var(--tr-base), var(--tr-pop), opacity .12s ease;
|
||||
}
|
||||
.drawflow .connection:hover foreignObject,
|
||||
.drawflow .connection:hover [class*="remove"],
|
||||
@@ -331,7 +414,7 @@ a.chip-btn:active {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.20), 0 4px 10px rgba(0,0,0,.35);
|
||||
box-shadow: var(--ring3-20-shadow);
|
||||
}
|
||||
/* If delete control is rendered inside foreignObject, normalize inner box */
|
||||
.drawflow .connection foreignObject div,
|
||||
@@ -685,7 +768,7 @@ a.chip-btn:active {
|
||||
/* Port hover affordance (no heavy effects) */
|
||||
.drawflow .drawflow-node .inputs .input,
|
||||
.drawflow .drawflow-node .outputs .output {
|
||||
transition: transform .08s ease;
|
||||
transition: var(--tr-pop-fast);
|
||||
will-change: transform;
|
||||
}
|
||||
.drawflow .drawflow-node .inputs .input:hover,
|
||||
@@ -712,18 +795,18 @@ a.chip-btn:active {
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.35) !important;
|
||||
cursor: pointer !important;
|
||||
z-index: 10 !important;
|
||||
transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease, border-color .12s ease, color .12s ease !important;
|
||||
transition: var(--tr-base), var(--tr-pop) !important;
|
||||
}
|
||||
.drawflow .drawflow-node .close:hover {
|
||||
transform: scale(1.06) !important;
|
||||
background: #1f2937 !important;
|
||||
border-color: var(--accent-2) !important;
|
||||
color: #f8fafc !important;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35) !important;
|
||||
box-shadow: var(--ring3-22-shadow) !important;
|
||||
}
|
||||
.drawflow .drawflow-node .close:active {
|
||||
transform: scale(0.98) !important;
|
||||
box-shadow: 0 0 0 2px rgba(96,165,250,.20), 0 2px 6px rgba(0,0,0,.35) !important;
|
||||
box-shadow: var(--ring2-20-shadow) !important;
|
||||
}
|
||||
/* Drawflow floating delete handle (class: .drawflow-delete) — restyle but keep behavior */
|
||||
#drawflow .drawflow-delete,
|
||||
@@ -741,7 +824,7 @@ a.chip-btn:active {
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.35) !important;
|
||||
cursor: pointer !important;
|
||||
z-index: 1000 !important;
|
||||
transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease, border-color .12s ease !important;
|
||||
transition: var(--tr-base), var(--tr-pop) !important;
|
||||
}
|
||||
#drawflow .drawflow-delete::before,
|
||||
.drawflow-delete::before {
|
||||
@@ -757,7 +840,7 @@ a.chip-btn:active {
|
||||
transform: translate(-50%, -50%) scale(1.06) !important;
|
||||
background: #1f2937 !important;
|
||||
border-color: var(--accent-2) !important;
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35) !important;
|
||||
box-shadow: var(--ring3-22-shadow) !important;
|
||||
}
|
||||
#drawflow .drawflow-delete:active,
|
||||
.drawflow-delete:active {
|
||||
@@ -766,7 +849,7 @@ a.chip-btn:active {
|
||||
/* Execution highlight states (SSE-driven) */
|
||||
.drawflow .drawflow-node .title-box,
|
||||
.drawflow .drawflow-node .box {
|
||||
transition: border-color .12s ease, box-shadow .12s ease, background-color .12s ease;
|
||||
transition: var(--tr-base);
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node.node-running .title-box,
|
||||
@@ -808,16 +891,6 @@ a.chip-btn:active {
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 1000; /* above nodes/edges but below menus */
|
||||
}
|
||||
/* Снимаем скролл-бары с контейнера Drawflow, чтобы не перекрывать правую стрелку */
|
||||
#drawflow {
|
||||
overflow: hidden !important;
|
||||
position: relative;
|
||||
z-index: 1; /* гарантируем, что канвас виден под HUD и над фоном */
|
||||
/* Растянем контейнер Drawflow на всю центральную колонку */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Panels collapse controls and layout */
|
||||
#container.collapse-left {
|
||||
@@ -1002,11 +1075,7 @@ select#pm-role {
|
||||
outline: none;
|
||||
font: 12px/1 Inter, system-ui, Arial, sans-serif;
|
||||
|
||||
transition:
|
||||
border-color .12s ease,
|
||||
box-shadow .12s ease,
|
||||
background-color .12s ease,
|
||||
color .12s ease;
|
||||
transition: var(--tr-base);
|
||||
}
|
||||
|
||||
/* Hover and focus states consistent with .top-input */
|
||||
@@ -1025,7 +1094,7 @@ select#vars-scope:focus,
|
||||
select.v-mode:focus,
|
||||
select#pm-role:focus {
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.20);
|
||||
box-shadow: var(--focus-ring3-20);
|
||||
}
|
||||
|
||||
/* Compact width contexts: keep natural width unless container forces 100% */
|
||||
@@ -1202,4 +1271,196 @@ header { position: relative; }
|
||||
}
|
||||
#inspector .var-row .v-del {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
/* --- Wire labels and arrows overlay --- */
|
||||
#wire-labels {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 4; /* над линиями, под панелями */
|
||||
}
|
||||
.wire-label {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #10151c;
|
||||
color: #e5e7eb;
|
||||
border: 1px solid rgba(148,163,184,.35);
|
||||
border-radius: 6px;
|
||||
padding: 1px 4px;
|
||||
font: 10px/1.2 Inter, system-ui, Arial, sans-serif;
|
||||
white-space: nowrap;
|
||||
opacity: .9;
|
||||
user-select: none;
|
||||
}
|
||||
.wire-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 8px solid var(--connector); /* перекрашивается inline из цвета линии */
|
||||
transform-origin: 50% 70%;
|
||||
opacity: .95;
|
||||
}
|
||||
/* Димминг посторонних связей при фокусе ноды */
|
||||
.drawflow .connection.dim .main-path {
|
||||
opacity: .35 !important;
|
||||
}
|
||||
|
||||
/* --- Сворачиваемые блоки превью в нодах --- */
|
||||
.np-coll { margin: 4px 0; }
|
||||
.np-coll > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
.np-coll > summary::-webkit-details-marker { display: none; }
|
||||
.np-coll[open] > summary { color: #cbd5e1; }
|
||||
|
||||
/* groups overlay removed */
|
||||
/* --- Canvas preview sanitization: hide hints/labels/checkboxes (only on canvas node previews) --- */
|
||||
/* Скрываем визуальные хинты, подписи и «галочки» только внутри превью нод на канвасе.
|
||||
Summary секции (headers/template) остаются видимыми, textarea/inputs продолжают отображать значения. */
|
||||
#canvas .drawflow .drawflow-node .node-preview .hint,
|
||||
#canvas .drawflow .drawflow-node .node-preview label,
|
||||
#canvas .drawflow .drawflow-node .node-preview input[type="checkbox"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* --- Unified checkbox style across UI --- */
|
||||
/* Единый тёмный стиль чекбоксов под тему проекта (акцент — var(--accent-2)).
|
||||
Применяется ко всей UI (инспектор, «Запуск», Prompt Blocks, STORE‑панель и т.д.).
|
||||
На канвасе в превью чекбоксы скрыты блоком выше. */
|
||||
input[type="checkbox"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: -2px;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 4px;
|
||||
background: #0f141a;
|
||||
box-shadow: 0 0 0 0 rgba(96,165,250,0.0);
|
||||
transition:
|
||||
background-color .12s ease,
|
||||
border-color .12s ease,
|
||||
box-shadow .12s ease,
|
||||
transform .06s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:hover {
|
||||
background: #121820;
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: 0 0 0 3px rgba(96,165,250,.18);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked {
|
||||
border-color: var(--accent-2);
|
||||
background-color: #0f141a;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2360a5fa' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: var(--focus-ring3-22);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:disabled {
|
||||
opacity: .6;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* --- Enhanced checkbox visual: add glowing blue dot at center --- */
|
||||
/* Применяется ко всем чекбоксам в UI (инспектор, Запуск, Prompt Blocks, STORE и т.д.).
|
||||
В превью нод на канвасе чекбоксы скрыты ранее добавленным правилом. */
|
||||
input[type="checkbox"] {
|
||||
position: relative; /* для центрирования псевдо-элемента */
|
||||
overflow: visible; /* безопасно для свечения */
|
||||
}
|
||||
input[type="checkbox"]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-2);
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
opacity: .6;
|
||||
/* мягкое синее свечение в покое */
|
||||
box-shadow:
|
||||
0 0 4px rgba(96,165,250,.45),
|
||||
0 0 10px rgba(96,165,250,.25);
|
||||
transition:
|
||||
transform .12s ease,
|
||||
opacity .12s ease,
|
||||
box-shadow .12s ease;
|
||||
}
|
||||
input[type="checkbox"]:checked::after {
|
||||
transform: translate(-50%, -50%) scale(1.0);
|
||||
opacity: 1;
|
||||
/* усиленное свечение при включении */
|
||||
box-shadow:
|
||||
0 0 6px rgba(96,165,250,.80),
|
||||
0 0 14px rgba(96,165,250,.60),
|
||||
0 0 24px rgba(96,165,250,.35);
|
||||
}
|
||||
input[type="checkbox"]:disabled::after {
|
||||
opacity: .35;
|
||||
box-shadow: 0 0 2px rgba(96,165,250,.25);
|
||||
}
|
||||
|
||||
/* --- Unified number input style across UI --- */
|
||||
/* Единый стиль для всех input[type=number], включая инспектор, «Запуск», SERVICE‑панели и т.д. */
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
background: #0f141a;
|
||||
color: #e5e7eb;
|
||||
border: 1px solid #2b3646;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
font: 12px/1 Inter, system-ui, Arial, sans-serif;
|
||||
transition: var(--tr-base);
|
||||
}
|
||||
input[type="number"]:hover {
|
||||
background: #121820;
|
||||
border-color: var(--accent-2);
|
||||
}
|
||||
input[type="number"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-2);
|
||||
box-shadow: var(--focus-ring3-20);
|
||||
}
|
||||
input[type="number"]:disabled {
|
||||
opacity: .6;
|
||||
cursor: not-allowed;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Убираем нативные «стрелочки», чтобы стиль был единым во всех браузерах */
|
||||
input[type="number"]::-webkit-outer-spin-button,
|
||||
input[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
/* --- Canvas preview sanitization (напоминание): хинты/лейблы/чекбоксы скрыты в превью --- */
|
||||
/* Секции summary (headers/template) остаются видимыми */
|
||||
1906
static/editor.html
1906
static/editor.html
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 200px; }
|
||||
|
||||
@@ -21,10 +21,15 @@
|
||||
// Готовим новые данные с глубокой копией blocks
|
||||
const newData = { ...(n.data || {}), blocks: Array.isArray(d2.blocks) ? d2.blocks.map(b => ({ ...b })) : [] };
|
||||
// 1) Обновляем внутреннее состояние Drawflow, чтобы export() возвращал актуальные данные
|
||||
try { editor.updateNodeDataFromId(id, newData); } catch (e) {}
|
||||
// 2) Обновляем DOM-отражение (источник правды для toPipelineJSON)
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
try {
|
||||
if (w.AU && typeof w.AU.updateNodeDataAndDom === 'function') {
|
||||
w.AU.updateNodeDataAndDom(editor, id, newData);
|
||||
} else {
|
||||
editor.updateNodeDataFromId(id, newData);
|
||||
const el2 = document.querySelector(`#node-${id}`);
|
||||
if (el2) el2.__data = JSON.parse(JSON.stringify(newData));
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) {}
|
||||
}
|
||||
// Initial sync to attach blocks into __data for toPipelineJSON
|
||||
|
||||
158
static/js/providerTemplates.js
Normal file
158
static/js/providerTemplates.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/* global window */
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
// Centralized registry for provider-specific defaults (base_url, endpoint, headers, template)
|
||||
// Exposes window.ProviderTemplates with:
|
||||
// .register(name, { defaultConfig: () => ({ base_url, endpoint, headers, template }) })
|
||||
// .defaults(provider)
|
||||
// .ensureConfigs(nodeData)
|
||||
// .getActiveProv(nodeData)
|
||||
// .getActiveCfg(nodeData)
|
||||
// .providers()
|
||||
|
||||
const PT = {};
|
||||
const _registry = new Map();
|
||||
|
||||
function norm(p) {
|
||||
return String(p == null ? 'openai' : p).toLowerCase().trim();
|
||||
}
|
||||
|
||||
PT.register = function register(name, def) {
|
||||
const key = norm(name);
|
||||
if (!def || typeof def.defaultConfig !== 'function') {
|
||||
throw new Error('ProviderTemplates.register: def.defaultConfig() required');
|
||||
}
|
||||
_registry.set(key, { defaultConfig: def.defaultConfig });
|
||||
};
|
||||
|
||||
PT.providers = function providers() {
|
||||
return Array.from(_registry.keys());
|
||||
};
|
||||
|
||||
PT.defaults = function defaults(provider) {
|
||||
const key = norm(provider);
|
||||
const rec = _registry.get(key);
|
||||
if (rec && typeof rec.defaultConfig === 'function') {
|
||||
try { return rec.defaultConfig(); } catch (_) {}
|
||||
}
|
||||
return { base_url: '', endpoint: '', headers: `{}`, template: `{}` };
|
||||
};
|
||||
|
||||
PT.ensureConfigs = function ensureConfigs(d) {
|
||||
if (!d) return;
|
||||
if (!d.provider) d.provider = 'openai';
|
||||
if (!d.provider_configs || typeof d.provider_configs !== 'object') d.provider_configs = {};
|
||||
for (const p of PT.providers()) {
|
||||
if (!d.provider_configs[p]) d.provider_configs[p] = PT.defaults(p);
|
||||
}
|
||||
};
|
||||
|
||||
PT.getActiveProv = function getActiveProv(d) {
|
||||
return norm(d && d.provider);
|
||||
};
|
||||
|
||||
PT.getActiveCfg = function getActiveCfg(d) {
|
||||
PT.ensureConfigs(d);
|
||||
const p = PT.getActiveProv(d);
|
||||
return d && d.provider_configs ? (d.provider_configs[p] || {}) : {};
|
||||
};
|
||||
|
||||
// --- Built-in providers (default presets) ---
|
||||
// Templates mirror original editor.html logic; use macros [[...]] and {{ ... }} as-is.
|
||||
function T_OPENAI() { return `{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
||||
"max_completion_tokens": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},
|
||||
"presence_penalty": {{ incoming.json.presence_penalty|default(0) }},
|
||||
"frequency_penalty": {{ incoming.json.frequency_penalty|default(0) }},
|
||||
"stop": {{ incoming.json.stop|default(params.stop|default([])) }},
|
||||
"stream": {{ incoming.json.stream|default(false) }}
|
||||
}`; }
|
||||
|
||||
function T_GEMINI() { return `{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"safetySettings": {{ incoming.json.safetySettings|default([]) }},
|
||||
"generationConfig": {
|
||||
"temperature": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},
|
||||
"topP": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},
|
||||
"maxOutputTokens": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},
|
||||
"stopSequences": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},
|
||||
"candidateCount": {{ incoming.json.generationConfig.candidateCount|default(1) }},
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},
|
||||
"thinkingBudget": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}
|
||||
}
|
||||
}
|
||||
}`; }
|
||||
|
||||
function T_GEMINI_IMAGE() { return `{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]]
|
||||
}`; }
|
||||
|
||||
function T_CLAUDE() { return `{
|
||||
"model": "{{ model }}",
|
||||
[[PROMPT]],
|
||||
"temperature": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},
|
||||
"top_p": {{ incoming.json.top_p|default(params.top_p|default(1)) }},
|
||||
"max_tokens": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},
|
||||
"stop_sequences": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},
|
||||
"stream": {{ incoming.json.stream|default(false) }},
|
||||
"thinking": {
|
||||
"type": "{{ incoming.json.thinking.type|default('disabled') }}",
|
||||
"budget_tokens": {{ incoming.json.thinking.budget_tokens|default(0) }}
|
||||
},
|
||||
"anthropic_version": "{{ anthropic_version|default('2023-06-01') }}"
|
||||
}`; }
|
||||
|
||||
// Register built-ins
|
||||
PT.register('openai', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://api.openai.com',
|
||||
endpoint: '/v1/chat/completions',
|
||||
headers: `{"Authorization":"Bearer [[VAR:incoming.headers.authorization]]"}`,
|
||||
template: T_OPENAI()
|
||||
})
|
||||
});
|
||||
PT.register('gemini', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://generativelanguage.googleapis.com',
|
||||
endpoint: '/v1beta/models/{{ model }}:generateContent?key=[[VAR:incoming.api_keys.key]]',
|
||||
headers: `{}`,
|
||||
template: T_GEMINI()
|
||||
})
|
||||
});
|
||||
PT.register('gemini_image', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://generativelanguage.googleapis.com',
|
||||
endpoint: '/v1beta/models/{{ model }}:generateContent',
|
||||
headers: `{"x-goog-api-key":"[[VAR:incoming.api_keys.key]]"}`,
|
||||
template: T_GEMINI_IMAGE()
|
||||
})
|
||||
});
|
||||
PT.register('claude', {
|
||||
defaultConfig: () => ({
|
||||
base_url: 'https://api.anthropic.com',
|
||||
endpoint: '/v1/messages',
|
||||
headers: `{"x-api-key":"[[VAR:incoming.headers.x-api-key]]","anthropic-version":"2023-06-01","anthropic-beta":"[[VAR:incoming.headers.anthropic-beta]]"}`,
|
||||
template: T_CLAUDE()
|
||||
})
|
||||
});
|
||||
|
||||
try { console.debug('[ProviderTemplates] providers:', PT.providers()); } catch (_) {}
|
||||
|
||||
// Export globals and compatibility shims
|
||||
try {
|
||||
w.ProviderTemplates = PT;
|
||||
// Back-compat shims so existing code can call global helpers
|
||||
w.providerDefaults = PT.defaults;
|
||||
w.ensureProviderConfigs = PT.ensureConfigs;
|
||||
w.getActiveProv = PT.getActiveProv;
|
||||
w.getActiveCfg = PT.getActiveCfg;
|
||||
} catch (_) {}
|
||||
})(window);
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
// Top-level pipeline meta kept in memory and included into JSON on save.
|
||||
// Allows UI to edit loop parameters without manual JSON edits.
|
||||
let _pipelineMeta = {
|
||||
// DRY: единый источник дефолтов и нормализации meta
|
||||
const MetaDefaults = Object.freeze({
|
||||
id: 'pipeline_editor',
|
||||
name: 'Edited Pipeline',
|
||||
parallel_limit: 8,
|
||||
@@ -20,19 +21,74 @@
|
||||
loop_max_iters: 1000,
|
||||
loop_time_budget_ms: 10000,
|
||||
clear_var_store: true,
|
||||
// New: default HTTP timeout for upstream requests (seconds)
|
||||
http_timeout_sec: 60,
|
||||
// New (v1): стратегия извлечения текста для [[OUTx]] (глобальная по умолчанию)
|
||||
// auto | deep | openai | gemini | claude | jsonpath
|
||||
text_extract_strategy: 'auto',
|
||||
// Используется при стратегии jsonpath (dot-нотация, поддержка индексов: a.b.0.c)
|
||||
text_extract_json_path: '',
|
||||
// Разделитель при объединении массива результатов
|
||||
text_join_sep: '\n',
|
||||
// v2: коллекция пресетов извлечения текста, управляется в "Запуск"
|
||||
// [{ id, name, strategy, json_path, join_sep }]
|
||||
text_extract_presets: [],
|
||||
};
|
||||
});
|
||||
|
||||
let _pipelineMeta = { ...MetaDefaults };
|
||||
|
||||
// Нормализатор meta: приводит типы, поддерживает синонимы ключей, заполняет дефолты
|
||||
function ensureMeta(p) {
|
||||
const src = (p && typeof p === 'object') ? p : {};
|
||||
const out = { ...MetaDefaults };
|
||||
|
||||
// helpers
|
||||
const toInt = (v, def) => {
|
||||
try {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) && n > 0 ? n : def;
|
||||
} catch { return def; }
|
||||
};
|
||||
const toNum = (v, def) => {
|
||||
try {
|
||||
const n = parseFloat(v);
|
||||
return !Number.isNaN(n) && n > 0 ? n : def;
|
||||
} catch { return def; }
|
||||
};
|
||||
|
||||
// базовые поля
|
||||
try { out.id = String((src.id ?? out.id) || out.id); } catch {}
|
||||
try { out.name = String((src.name ?? out.name) || out.name); } catch {}
|
||||
|
||||
out.parallel_limit = toInt(src.parallel_limit, out.parallel_limit);
|
||||
out.loop_mode = String((src.loop_mode ?? out.loop_mode) || out.loop_mode);
|
||||
out.loop_max_iters = toInt(src.loop_max_iters, out.loop_max_iters);
|
||||
out.loop_time_budget_ms = toInt(src.loop_time_budget_ms, out.loop_time_budget_ms);
|
||||
out.clear_var_store = (typeof src.clear_var_store === 'boolean') ? !!src.clear_var_store : out.clear_var_store;
|
||||
out.http_timeout_sec = toNum(src.http_timeout_sec, out.http_timeout_sec);
|
||||
out.text_extract_strategy = String((src.text_extract_strategy ?? out.text_extract_strategy) || out.text_extract_strategy);
|
||||
out.text_extract_json_path = String((src.text_extract_json_path ?? out.text_extract_json_path) || out.text_extract_json_path);
|
||||
|
||||
// поддержка синонимов text_join_sep (регистр и вариации)
|
||||
let joinSep = out.text_join_sep;
|
||||
try {
|
||||
for (const k of Object.keys(src)) {
|
||||
if (String(k).toLowerCase() === 'text_join_sep') { joinSep = src[k]; break; }
|
||||
}
|
||||
} catch {}
|
||||
out.text_join_sep = String((joinSep ?? src.text_join_sep ?? out.text_join_sep) || out.text_join_sep);
|
||||
|
||||
// коллекция пресетов
|
||||
try {
|
||||
const arr = Array.isArray(src.text_extract_presets) ? src.text_extract_presets : [];
|
||||
out.text_extract_presets = arr
|
||||
.filter(it => it && typeof it === 'object')
|
||||
.map((it, idx) => ({
|
||||
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2) + idx)),
|
||||
name: String(it.name ?? (it.json_path || 'Preset')),
|
||||
strategy: String(it.strategy ?? 'auto'),
|
||||
json_path: String(it.json_path ?? ''),
|
||||
join_sep: String(it.join_sep ?? '\n'),
|
||||
}));
|
||||
} catch { out.text_extract_presets = []; }
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function getPipelineMeta() {
|
||||
return { ..._pipelineMeta };
|
||||
@@ -40,48 +96,8 @@
|
||||
|
||||
function updatePipelineMeta(p) {
|
||||
if (!p || typeof p !== 'object') return;
|
||||
const keys = [
|
||||
'id','name','parallel_limit','loop_mode','loop_max_iters','loop_time_budget_ms','clear_var_store','http_timeout_sec',
|
||||
'text_extract_strategy','text_extract_json_path','text_join_sep','text_join_sep','text_join_SEP',
|
||||
// v2 presets collection
|
||||
'text_extract_presets'
|
||||
];
|
||||
for (const k of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(p, k) && p[k] !== undefined && p[k] !== null && (k === 'clear_var_store' ? true : p[k] !== '')) {
|
||||
if (k === 'parallel_limit' || k === 'loop_max_iters' || k === 'loop_time_budget_ms') {
|
||||
const v = parseInt(p[k], 10);
|
||||
if (!Number.isNaN(v) && v > 0) _pipelineMeta[k] = v;
|
||||
} else if (k === 'http_timeout_sec') {
|
||||
const fv = parseFloat(p[k]);
|
||||
if (!Number.isNaN(fv) && fv > 0) _pipelineMeta[k] = fv;
|
||||
} else if (k === 'clear_var_store') {
|
||||
_pipelineMeta[k] = !!p[k];
|
||||
} else {
|
||||
// спец-обработка коллекции пресетов
|
||||
if (k === 'text_extract_presets') {
|
||||
try {
|
||||
const arr = Array.isArray(p[k]) ? p[k] : [];
|
||||
_pipelineMeta[k] = arr
|
||||
.filter(it => it && typeof it === 'object')
|
||||
.map(it => ({
|
||||
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2))),
|
||||
name: String(it.name ?? 'Preset'),
|
||||
strategy: String(it.strategy ?? 'auto'),
|
||||
json_path: String(it.json_path ?? ''),
|
||||
join_sep: String(it.join_sep ?? '\n'),
|
||||
}));
|
||||
} catch (_) {
|
||||
_pipelineMeta[k] = [];
|
||||
}
|
||||
} else if (k.toLowerCase() === 'text_join_sep') {
|
||||
// нормализация ключа join separator (допускаем разные написания)
|
||||
_pipelineMeta['text_join_sep'] = String(p[k]);
|
||||
} else {
|
||||
_pipelineMeta[k] = String(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// DRY: единая точка нормализации
|
||||
_pipelineMeta = ensureMeta({ ..._pipelineMeta, ...p });
|
||||
}
|
||||
|
||||
// Drawflow -> pipeline JSON
|
||||
@@ -260,24 +276,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Собираем итоговый pipeline JSON с метаданными
|
||||
const meta = getPipelineMeta();
|
||||
return {
|
||||
id: meta.id || 'pipeline_editor',
|
||||
name: meta.name || 'Edited Pipeline',
|
||||
parallel_limit: (typeof meta.parallel_limit === 'number' ? meta.parallel_limit : 8),
|
||||
loop_mode: (meta.loop_mode || 'dag'),
|
||||
loop_max_iters: (typeof meta.loop_max_iters === 'number' ? meta.loop_max_iters : 1000),
|
||||
loop_time_budget_ms: (typeof meta.loop_time_budget_ms === 'number' ? meta.loop_time_budget_ms : 10000),
|
||||
clear_var_store: (typeof meta.clear_var_store === 'boolean' ? meta.clear_var_store : true),
|
||||
http_timeout_sec: (typeof meta.http_timeout_sec === 'number' ? meta.http_timeout_sec : 60),
|
||||
text_extract_strategy: (meta.text_extract_strategy || 'auto'),
|
||||
text_extract_json_path: (meta.text_extract_json_path || ''),
|
||||
text_join_sep: (meta.text_join_sep || '\n'),
|
||||
// v2: persist presets
|
||||
text_extract_presets: (Array.isArray(meta.text_extract_presets) ? meta.text_extract_presets : []),
|
||||
nodes
|
||||
};
|
||||
// 3) Собираем итоговый pipeline JSON с метаданными (нормализованными)
|
||||
const meta = ensureMeta(getPipelineMeta());
|
||||
try { console.debug('[AgentUISer.toPipelineJSON] meta_keys', Object.keys(meta || {})); } catch (e) {}
|
||||
return { ...meta, nodes };
|
||||
}
|
||||
|
||||
// pipeline JSON -> Drawflow
|
||||
@@ -285,25 +287,25 @@
|
||||
ensureDeps();
|
||||
const editor = w.editor;
|
||||
const NODE_IO = w.NODE_IO;
|
||||
|
||||
// Сохраняем метаданные пайплайна для UI
|
||||
try {
|
||||
updatePipelineMeta({
|
||||
id: p && p.id ? p.id : 'pipeline_editor',
|
||||
name: p && p.name ? p.name : 'Edited Pipeline',
|
||||
parallel_limit: (p && typeof p.parallel_limit === 'number') ? p.parallel_limit : 8,
|
||||
loop_mode: p && p.loop_mode ? p.loop_mode : 'dag',
|
||||
loop_max_iters: (p && typeof p.loop_max_iters === 'number') ? p.loop_max_iters : 1000,
|
||||
loop_time_budget_ms: (p && typeof p.loop_time_budget_ms === 'number') ? p.loop_time_budget_ms : 10000,
|
||||
clear_var_store: (p && typeof p.clear_var_store === 'boolean') ? p.clear_var_store : true,
|
||||
http_timeout_sec: (p && typeof p.http_timeout_sec === 'number') ? p.http_timeout_sec : 60,
|
||||
text_extract_strategy: (p && typeof p.text_extract_strategy === 'string') ? p.text_extract_strategy : 'auto',
|
||||
text_extract_json_path: (p && typeof p.text_extract_json_path === 'string') ? p.text_extract_json_path : '',
|
||||
text_join_sep: (p && typeof p.text_join_sep === 'string') ? p.text_join_sep : '\n',
|
||||
// v2: presets from pipeline.json
|
||||
text_extract_presets: (p && Array.isArray(p.text_extract_presets)) ? p.text_extract_presets : [],
|
||||
});
|
||||
} catch (e) {}
|
||||
// Сохраняем метаданные пайплайна для UI (сквозная нормализация)
|
||||
try {
|
||||
updatePipelineMeta(p || {});
|
||||
// Диагностический лог состава meta для подтверждения DRY-рефакторинга
|
||||
try {
|
||||
const metaKeys = ["id","name","parallel_limit","loop_mode","loop_max_iters","loop_time_budget_ms","clear_var_store","http_timeout_sec","text_extract_strategy","text_extract_json_path","text_join_sep","text_extract_presets"];
|
||||
const incomingKeys = metaKeys.filter(k => (p && Object.prototype.hasOwnProperty.call(p, k)));
|
||||
const currentMeta = (typeof getPipelineMeta === 'function') ? getPipelineMeta() : {};
|
||||
console.debug('[AgentUISer.fromPipelineJSON] meta_keys', {
|
||||
incomingKeys,
|
||||
resultKeys: Object.keys(currentMeta || {}),
|
||||
metaPreview: {
|
||||
id: currentMeta && currentMeta.id,
|
||||
loop_mode: currentMeta && currentMeta.loop_mode,
|
||||
http_timeout_sec: currentMeta && currentMeta.http_timeout_sec
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
} catch (e) {}
|
||||
|
||||
editor.clear();
|
||||
let x = 100; let y = 120; // Fallback
|
||||
|
||||
213
static/js/utils.js
Normal file
213
static/js/utils.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/* global window */
|
||||
// AgentUI common UI utilities (DRY helpers shared by editor.html and pm-ui.js)
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
const AU = {};
|
||||
|
||||
// HTML escaping for safe text/attribute insertion
|
||||
AU.escapeHtml = function escapeHtml(s) {
|
||||
const str = String(s ?? '');
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Attribute-safe escape (keeps quotes escaped; conservative)
|
||||
AU.escAttr = function escAttr(v) {
|
||||
const s = String(v ?? '');
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
// Text-node escape (keeps quotes as-is for readability)
|
||||
AU.escText = function escText(v) {
|
||||
const s = String(v ?? '');
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
};
|
||||
|
||||
// DRY helper: sync Drawflow node data + mirror into DOM.__data with deep copy
|
||||
AU.updateNodeDataAndDom = function updateNodeDataAndDom(editor, id, data) {
|
||||
try { editor && typeof editor.updateNodeDataFromId === 'function' && editor.updateNodeDataFromId(id, data); } catch (_) {}
|
||||
try {
|
||||
const el = document.querySelector('#node-' + id);
|
||||
if (el) el.__data = JSON.parse(JSON.stringify(data));
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
// Double rAF helper: waits for two animation frames; returns Promise or accepts callback
|
||||
AU.nextRaf2 = function nextRaf2(cb) {
|
||||
try {
|
||||
if (typeof requestAnimationFrame === 'function') {
|
||||
if (typeof cb === 'function') {
|
||||
requestAnimationFrame(() => { requestAnimationFrame(() => { try { cb(); } catch (_) {} }); });
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
||||
} else {
|
||||
if (typeof cb === 'function') { setTimeout(() => { try { cb(); } catch (_) {} }, 32); return; }
|
||||
return new Promise((resolve) => setTimeout(resolve, 32));
|
||||
}
|
||||
} catch (_) {
|
||||
if (typeof cb === 'function') { try { cb(); } catch (__ ) {} }
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
// Heuristic: looks like long base64 payload
|
||||
AU.isProbablyBase64 = function isProbablyBase64(s) {
|
||||
try {
|
||||
if (typeof s !== 'string') return false;
|
||||
if (s.length < 64) return false;
|
||||
return /^[A-Za-z0-9+/=\r\n]+$/.test(s);
|
||||
} catch { return false; }
|
||||
};
|
||||
|
||||
AU.trimBase64 = function trimBase64(s, maxLen = 180) {
|
||||
try {
|
||||
const str = String(s ?? '');
|
||||
if (str.length > maxLen) {
|
||||
return str.slice(0, maxLen) + `... (trimmed ${str.length - maxLen})`;
|
||||
}
|
||||
return str;
|
||||
} catch { return String(s ?? ''); }
|
||||
};
|
||||
|
||||
// Flatten JSON-like object into [path, stringValue] pairs
|
||||
// Includes special handling for backend preview objects: { "__truncated__": true, "preview": "..." }
|
||||
AU.flattenObject = function flattenObject(obj, prefix = '') {
|
||||
const out = [];
|
||||
if (obj == null) return out;
|
||||
if (typeof obj !== 'object') {
|
||||
out.push([prefix, String(obj)]);
|
||||
return out;
|
||||
}
|
||||
try {
|
||||
const entries = Object.entries(obj);
|
||||
for (const [k, v] of entries) {
|
||||
const p = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||
// Special preview shape from backend
|
||||
if (Object.prototype.hasOwnProperty.call(v, '__truncated__') && Object.prototype.hasOwnProperty.call(v, 'preview')) {
|
||||
out.push([p, String(v.preview ?? '')]);
|
||||
continue;
|
||||
}
|
||||
out.push(...AU.flattenObject(v, p));
|
||||
} else {
|
||||
try {
|
||||
const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 0);
|
||||
out.push([p, s]);
|
||||
} catch {
|
||||
out.push([p, String(v)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback best-effort
|
||||
try { out.push([prefix, JSON.stringify(obj)]); } catch { out.push([prefix, String(obj)]); }
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
// Format headers dictionary into text lines "Key: Value"
|
||||
AU.fmtHeaders = function fmtHeaders(h) {
|
||||
try {
|
||||
const keys = Object.keys(h || {});
|
||||
return keys.map(k => `${k}: ${String(h[k])}`).join('\n');
|
||||
} catch { return ''; }
|
||||
};
|
||||
|
||||
// Build HTTP request preview text
|
||||
AU.buildReqText = function buildReqText(x) {
|
||||
if (!x) return '';
|
||||
const head = `${x.method || 'POST'} ${x.url || '/'} HTTP/1.1`;
|
||||
const host = (() => {
|
||||
try { const u = new URL(x.url); return `Host: ${u.host}`; } catch { return ''; }
|
||||
})();
|
||||
const hs = AU.fmtHeaders(x.headers || {});
|
||||
const body = String(x.body_text || '').trim();
|
||||
return [head, host, hs, '', body].filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
// Build HTTP response preview text
|
||||
AU.buildRespText = function buildRespText(x) {
|
||||
if (!x) return '';
|
||||
const head = `HTTP/1.1 ${x.status || 0}`;
|
||||
const hs = AU.fmtHeaders(x.headers || {});
|
||||
const body = String(x.body_text || '').trim();
|
||||
return [head, hs, '', body].filter(Boolean).join('\n');
|
||||
};
|
||||
|
||||
// Unified fetch helper with timeout and JSON handling
|
||||
AU.apiFetch = async function apiFetch(url, opts) {
|
||||
const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
||||
const o = opts || {};
|
||||
const method = String(o.method || 'GET').toUpperCase();
|
||||
const expectJson = (o.expectJson !== false); // default true
|
||||
const headers = Object.assign({}, o.headers || {});
|
||||
let body = o.body;
|
||||
const timeoutMs = Number.isFinite(o.timeoutMs) ? o.timeoutMs : 15000;
|
||||
|
||||
const hasAbort = (typeof AbortController !== 'undefined');
|
||||
const ctrl = hasAbort ? new AbortController() : null;
|
||||
let to = null;
|
||||
if (ctrl) {
|
||||
try { to = setTimeout(() => { try { ctrl.abort(); } catch(_){} }, timeoutMs); } catch(_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
if (expectJson) {
|
||||
if (!headers['Accept'] && !headers['accept']) headers['Accept'] = 'application/json';
|
||||
}
|
||||
if (body != null) {
|
||||
const isForm = (typeof FormData !== 'undefined' && body instanceof FormData);
|
||||
const isBlob = (typeof Blob !== 'undefined' && body instanceof Blob);
|
||||
if (typeof body === 'object' && !isForm && !isBlob) {
|
||||
body = JSON.stringify(body);
|
||||
if (!headers['Content-Type'] && !headers['content-type']) headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(url, { method, headers, body, signal: ctrl ? ctrl.signal : undefined });
|
||||
const ct = String(res.headers && res.headers.get ? (res.headers.get('Content-Type') || '') : '');
|
||||
const isJsonCt = /application\/json/i.test(ct);
|
||||
|
||||
let data = null;
|
||||
if (expectJson || isJsonCt) {
|
||||
try { data = await res.json(); } catch (_) { data = null; }
|
||||
} else {
|
||||
try { data = await res.text(); } catch (_) { data = null; }
|
||||
}
|
||||
|
||||
const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
||||
try { console.debug('[AU.apiFetch]', { method, url, status: res.status, ms: Math.round(t1 - t0) }); } catch(_) {}
|
||||
|
||||
if (!res.ok) {
|
||||
const msg = (data && typeof data === 'object' && data.error) ? String(data.error) : `HTTP ${res.status}`;
|
||||
const err = new Error(`apiFetch: ${msg}`);
|
||||
err.status = res.status;
|
||||
err.data = data;
|
||||
err.url = url;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return data;
|
||||
} finally {
|
||||
if (to) { try { clearTimeout(to); } catch(_) {} }
|
||||
}
|
||||
};
|
||||
|
||||
// Expose
|
||||
try { w.AU = AU; } catch (_) {}
|
||||
try { w.nextRaf2 = AU.nextRaf2; } catch (_) {}
|
||||
})(window);
|
||||
@@ -4,6 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>НадTavern — Pipeline Editor (JSON)</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 24px; }
|
||||
textarea { width: 100%; height: 70vh; }
|
||||
|
||||
Reference in New Issue
Block a user