Split product system into editable sections
This commit is contained in:
parent
d0020a48e3
commit
3e789d6887
|
|
@ -123,11 +123,17 @@ const GROUP_COPY = {
|
|||
"Юридический блок": "Политики, права и нижний текст формы.",
|
||||
"Видео-окно": "Окно демо-видео и подключенный медиа-файл.",
|
||||
Пункты: "Текстовые пункты внутри секции.",
|
||||
Слайдер: "Видео-превью и подписи карточек внутри горизонтального слайдера.",
|
||||
"Один клик": "Подблок про быстрое добавление и настройку компонентов.",
|
||||
MCP: "Подблок про MCP-сервер и контекст Webflow.",
|
||||
FAQ: "Список вопросов и тексты ответов в mail-интерфейсе.",
|
||||
CTA: "Финальный призыв к действию: заголовок, пояснение, кнопка и ссылка.",
|
||||
Тарифы: "Верхняя часть тарифной таблицы: заголовки, планы, цены, кнопки и примечания.",
|
||||
Контент: "Остальные typed-поля выбранного блока.",
|
||||
};
|
||||
|
||||
function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null) {
|
||||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path) };
|
||||
function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null, options = {}) {
|
||||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options };
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
|
|
@ -171,6 +177,12 @@ function inferFieldGroup(path) {
|
|||
if (path === "content.legalHtml") return "Юридический блок";
|
||||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||||
if (path.startsWith("content.features.")) return "Пункты";
|
||||
if (path.startsWith("content.sliderItems.")) return "Слайдер";
|
||||
if (path.startsWith("content.oneClick.")) return "Один клик";
|
||||
if (path.startsWith("content.mcp.")) return "MCP";
|
||||
if (path.startsWith("content.faq.")) return "FAQ";
|
||||
if (path.startsWith("content.cta.")) return "CTA";
|
||||
if (path.startsWith("content.plans.") || path.startsWith("content.pricing.")) return "Тарифы";
|
||||
if (path.startsWith("content.heading.") || path === "content.introHtml") return "Заголовок";
|
||||
return "Контент";
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
|||
<section class="s"><div class="c _w-60"><hgroup data-module="hg" class="ho"><h2 class="main-h h2"><span class="darker-40">{{text:content.heading.muted}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div></hgroup></div><div class="c full"><div class="w-dyn-list"><div data-module="slider" role="list" class="slider w-dyn-items"><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.0.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.0.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.1.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.1.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.2.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.2.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.3.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.3.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.4.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.4.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.5.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.5.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.6.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.6.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.7.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.7.title}}</h3></hgroup></div></div></div></div><div class="webgl-target-w"><div data-scale="1, 0.6" data-module="webgl-target" class="webgl-target"></div></div></section>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<section class="s"><div class="c _w-60"><hgroup data-module="hg" class="ho flip"><h2 class="main-h h1"><span class="darker-40">{{text:content.heading.muted}}</span><span class="darker-70">{{text:content.heading.middle}}</span><span>{{text:content.heading.main}}</span></h2><div class="h-txt">{{html:content.introHtml}}</div></hgroup></div><div class="c full"><div class="w-dyn-list"><div data-module="slider" role="list" class="slider w-dyn-items"><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.0.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.0.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.1.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.1.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.2.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.2.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.3.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.3.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.4.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.4.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.5.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.5.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.6.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.6.title}}</h3></hgroup></div><div role="listitem" class="slide-w w-dyn-item"><div class="video-cut"><div class="video-screen"><video src="{{attr:content.sliderItems.7.videoSrc}}" playsinline="true" muted="" loop="" preload="none" data-module="video-handle" class="video-el"></video></div></div><hgroup class="slide-he-w"><h3 data-title="" class="slide-he">{{text:content.sliderItems.7.title}}</h3></hgroup></div></div></div></div><div class="gap-v"><div class="c _w-60"><hgroup data-module="hg" class="ho"><h2 class="main-h h2"><span class="darker-40">{{text:content.oneClick.heading.muted}}</span><span class="darker-70">{{text:content.oneClick.heading.middle}}</span><span>{{text:content.oneClick.heading.main}}</span></h2><div class="txt-w"><div class="h-txt">{{html:content.oneClick.introHtml}}</div><div class="hg-par darker-40">{{text:content.oneClick.items.0.eyebrow}}</div><div class="h-txt">{{html:content.oneClick.items.0.bodyHtml}}</div><div class="hg-par darker-40">{{text:content.oneClick.items.1.eyebrow}}</div><div class="h-txt">{{html:content.oneClick.items.1.bodyHtml}}</div></div></hgroup></div><div class="c _w-60"><hgroup data-module="hg" class="ho flip"><h2 class="main-h h2"><span class="darker-40">{{text:content.mcp.heading.muted}}</span><span class="darker-70">{{text:content.mcp.heading.middle}}</span><span>{{text:content.mcp.heading.main}}</span></h2><div class="txt-w"><div class="h-txt">{{html:content.mcp.introHtml}}</div><div class="hg-par darker-40">{{text:content.mcp.items.0.eyebrow}}</div><div class="h-txt">{{html:content.mcp.items.0.bodyHtml}}</div></div></hgroup></div></div></section>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
<section class="s"><div class="c py"><hgroup data-module="hg" class="ho"><h2 class="main-h h1"><span class="darker-40">{{text:content.cta.heading.muted}}</span><span class="darker-70">{{text:content.cta.heading.middle}}</span><span>{{text:content.cta.heading.main}}</span></h2><div><div class="h-txt">{{text:content.cta.intro}}</div><div class="h-txt">{{html:content.cta.detailHtml}}</div><a data-module="btn" href="{{attr:content.cta.href}}" class="form-btn static mt-10 w-inline-block"><div data-module="alpha" class="btn-bg"><div class="clip-12"><div>{{text:content.cta.buttonLabel}}</div></div></div></a></div></hgroup></div></section>
|
||||
|
|
@ -38,6 +38,17 @@ const waitlistFilesStart = findRequired(marker.waitlistFiles, pricingIntroStart)
|
|||
const demoVideoFolderStart = findRequired(marker.demoVideoFolder, waitlistFilesStart);
|
||||
const footerStart = findRequired(marker.footer, demoVideoFolderStart);
|
||||
const footerEnd = findRequired("</footer>", footerStart) + "</footer>".length;
|
||||
const productSystemHtml = html.slice(cVizOpenEnd, pricingIntroStart);
|
||||
const productSectionStarts = [...productSystemHtml.matchAll(/<section\b/g)].map((match) => match.index);
|
||||
|
||||
if (productSectionStarts.length < 5) {
|
||||
throw new Error(`Expected at least 5 product-system sections, found ${productSectionStarts.length}`);
|
||||
}
|
||||
|
||||
const productSections = productSectionStarts.map((start, index) => {
|
||||
const end = productSectionStarts[index + 1] ?? productSystemHtml.length;
|
||||
return productSystemHtml.slice(start, end);
|
||||
});
|
||||
|
||||
let shell = [
|
||||
html.slice(0, heroStart),
|
||||
|
|
@ -68,12 +79,48 @@ const blocks = [
|
|||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "product-system",
|
||||
id: "component-library",
|
||||
type: "staticHtml",
|
||||
template: "product-system.html",
|
||||
adminLabel: "Компоненты, режимы, FAQ и тарифная таблица",
|
||||
template: "component-library.html",
|
||||
adminLabel: "Компонентная библиотека и MCP",
|
||||
anchor: null,
|
||||
html: html.slice(cVizOpenEnd, pricingIntroStart),
|
||||
html: productSections[0],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "ai-modes",
|
||||
type: "staticHtml",
|
||||
template: "ai-modes.html",
|
||||
adminLabel: "Гибкие режимы ИИ",
|
||||
anchor: null,
|
||||
html: productSections[1],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "faq",
|
||||
type: "staticHtml",
|
||||
template: "faq.html",
|
||||
adminLabel: "Частые вопросы и ответы",
|
||||
anchor: null,
|
||||
html: productSections[2],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "publish-cta",
|
||||
type: "staticHtml",
|
||||
template: "publish-cta.html",
|
||||
adminLabel: "Финальный CTA перед тарифами",
|
||||
anchor: "hero",
|
||||
html: productSections[3],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
id: "pricing-table",
|
||||
type: "staticHtml",
|
||||
template: "pricing-table.html",
|
||||
adminLabel: "Тарифная таблица",
|
||||
anchor: "pricing-wrap",
|
||||
html: productSections[4],
|
||||
},
|
||||
{
|
||||
region: "cViz",
|
||||
|
|
@ -128,12 +175,18 @@ function inferFieldGroup(path) {
|
|||
if (path === "content.legalHtml") return "Юридический блок";
|
||||
if (path === "content.windowTitle" || path === "content.videoSrc") return "Видео-окно";
|
||||
if (path.startsWith("content.features.")) return "Пункты";
|
||||
if (path.startsWith("content.sliderItems.")) return "Слайдер";
|
||||
if (path.startsWith("content.oneClick.")) return "Один клик";
|
||||
if (path.startsWith("content.mcp.")) return "MCP";
|
||||
if (path.startsWith("content.faq.")) return "FAQ";
|
||||
if (path.startsWith("content.cta.")) return "CTA";
|
||||
if (path.startsWith("content.plans.") || path.startsWith("content.pricing.")) return "Тарифы";
|
||||
if (path.startsWith("content.heading.") || path === "content.introHtml") return "Заголовок";
|
||||
return "Контент";
|
||||
}
|
||||
|
||||
function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null) {
|
||||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path) };
|
||||
function field(path, label, kind = "text", renderAs = kind === "html" ? "html" : "text", group = null, options = {}) {
|
||||
return { path, label, kind, renderAs, group: group || inferFieldGroup(path), ...options };
|
||||
}
|
||||
|
||||
function tokenizeBlock(block, definition) {
|
||||
|
|
@ -151,7 +204,7 @@ function tokenizeBlock(block, definition) {
|
|||
throw new Error(`Could not find "${value}" while tokenizing ${block.id}`);
|
||||
}
|
||||
|
||||
html = html.replace(value, token);
|
||||
html = editableField.replaceAll ? html.split(value).join(token) : html.replace(value, token);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -163,6 +216,24 @@ function tokenizeBlock(block, definition) {
|
|||
};
|
||||
}
|
||||
|
||||
const demoSliderItems = [
|
||||
{ videoSrc: "./assets/media/various-modes.mp4", title: "Переключение режимов ИИ" },
|
||||
{ videoSrc: "./assets/media/project-flow.mp4", title: "Создание нового проекта" },
|
||||
{ videoSrc: "./assets/media/ai-dock-mode.mp4", title: "ИИ-док" },
|
||||
{ videoSrc: "./assets/media/account.mp4", title: "Аккаунт" },
|
||||
{ videoSrc: "./assets/media/code-edits.mp4", title: "Редактирование кода с ИИ" },
|
||||
{ videoSrc: "./assets/media/dev-server.mp4", title: "Dev-сервер с ИИ" },
|
||||
{ videoSrc: "./assets/media/webflow-mcp.mp4", title: "Webflow MCP" },
|
||||
{ videoSrc: "./assets/media/publish-flow.mp4", title: "Публикация" },
|
||||
];
|
||||
|
||||
function sliderEditableFields(prefix = "content.sliderItems", labelPrefix = "Слайд") {
|
||||
return demoSliderItems.flatMap((_, index) => [
|
||||
field(`${prefix}.${index}.videoSrc`, `${labelPrefix} ${index + 1}: видео`, "media", "attr"),
|
||||
field(`${prefix}.${index}.title`, `${labelPrefix} ${index + 1}: подпись`),
|
||||
]);
|
||||
}
|
||||
|
||||
const typedBlockDefinitions = {
|
||||
"hero-waitlist": {
|
||||
type: "heroWaitlist",
|
||||
|
|
@ -300,6 +371,279 @@ const typedBlockDefinitions = {
|
|||
field("content.features.3.bodyHtml", "Пункт 4: текст", "html"),
|
||||
],
|
||||
},
|
||||
"component-library": {
|
||||
type: "componentLibrary",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Агентная",
|
||||
middle: "Компонентная",
|
||||
main: "библиотека.",
|
||||
},
|
||||
introHtml:
|
||||
'Готовая к работе с агентами <span class="darker-70">компонентная</span> библиотека: добавление в один клик через приложение NODE.DC для Webflow и дальнейшая настройка под проект.',
|
||||
sliderItems: demoSliderItems,
|
||||
oneClick: {
|
||||
heading: {
|
||||
muted: "Один",
|
||||
middle: "клик",
|
||||
main: "добавления.",
|
||||
},
|
||||
introHtml: 'С помощью <span class="darker-70">расширения NODE.DC для Webflow.</span> ',
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Понимание компонентов",
|
||||
bodyHtml:
|
||||
'Используйте <span class="darker-70">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность.',
|
||||
},
|
||||
{
|
||||
eyebrow: "Быстрый старт",
|
||||
bodyHtml:
|
||||
'Используйте быстрые <span class="darker-70">Bun</span> сборки и автообновление для вашего <span class="darker-70">кастомного кода.</span>',
|
||||
},
|
||||
],
|
||||
},
|
||||
mcp: {
|
||||
heading: {
|
||||
muted: "Получите",
|
||||
middle: "более сильный",
|
||||
main: "MCP.",
|
||||
},
|
||||
introHtml: "Полностью переработанный и более быстрый MCP-сервер. ",
|
||||
items: [
|
||||
{
|
||||
eyebrow: "Понимание Webflow",
|
||||
bodyHtml:
|
||||
'Используйте <span class="darker-70">несколько агентов параллельно</span> чтобы добавлять кастомный код, анимации и функциональность.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.middle", "Заголовок: средняя строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
...sliderEditableFields(),
|
||||
field("content.oneClick.heading.muted", "Один клик: приглушённая строка"),
|
||||
field("content.oneClick.heading.middle", "Один клик: средняя строка"),
|
||||
field("content.oneClick.heading.main", "Один клик: основная строка"),
|
||||
field("content.oneClick.introHtml", "Один клик: вводный текст", "html"),
|
||||
field("content.oneClick.items.0.eyebrow", "Один клик: пункт 1, подпись"),
|
||||
field("content.oneClick.items.0.bodyHtml", "Один клик: пункт 1, текст", "html"),
|
||||
field("content.oneClick.items.1.eyebrow", "Один клик: пункт 2, подпись"),
|
||||
field("content.oneClick.items.1.bodyHtml", "Один клик: пункт 2, текст", "html"),
|
||||
field("content.mcp.heading.muted", "MCP: приглушённая строка"),
|
||||
field("content.mcp.heading.middle", "MCP: средняя строка"),
|
||||
field("content.mcp.heading.main", "MCP: основная строка"),
|
||||
field("content.mcp.introHtml", "MCP: вводный текст", "html"),
|
||||
field("content.mcp.items.0.eyebrow", "MCP: пункт 1, подпись"),
|
||||
field("content.mcp.items.0.bodyHtml", "MCP: пункт 1, текст", "html"),
|
||||
],
|
||||
},
|
||||
"ai-modes": {
|
||||
type: "aiModes",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Гибкие",
|
||||
main: "режимы ИИ.",
|
||||
},
|
||||
introHtml: 'Разные <span class="darker-70">режимы</span> и <span class="darker-70">модели</span> для разных задач.',
|
||||
sliderItems: demoSliderItems,
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
...sliderEditableFields(),
|
||||
],
|
||||
},
|
||||
faq: {
|
||||
type: "faqSection",
|
||||
content: {
|
||||
heading: {
|
||||
muted: "Частые",
|
||||
middle: "вопросы",
|
||||
main: "и ответы",
|
||||
},
|
||||
introHtml: '<span class="darker-70">Подробный обзор</span> возможностей, логики работы и внутреннего устройства.',
|
||||
docsHref: "https://docs.ssscript.app/",
|
||||
docsLabel: "Полная документация",
|
||||
windowTitle: "FAQ",
|
||||
faq: [
|
||||
{
|
||||
question: "Это влияет на производительность?",
|
||||
answerHtml:
|
||||
"<p>Как и любой кастомный код, это добавляет только ту нагрузку, которую несёт сам код. </p><p>Все файлы собираются, минифицируются и сжимаются, чтобы минимально влиять на проект и не плодить внешние библиотеки и CDN-скрипты.</p>",
|
||||
},
|
||||
{
|
||||
question: "Где размещаются мои файлы?",
|
||||
answerHtml:
|
||||
"<p>После сборки файлы загружаются в Cloudflare R2 и отдаются через CDN-ссылку на выделенном домене проекта, чтобы получить быстрый и совместимый с Webflow результат.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что делать, если я опубликовал не те файлы?",
|
||||
answerHtml:
|
||||
"<p>В панели деплоя есть мгновенный откат. </p><p>Нажмите откат, верните предыдущую рабочую версию, исправьте локально и опубликуйте заново.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что такое ИИ-агент Webflow?",
|
||||
answerHtml:
|
||||
"<p>Помимо <strong>@plan</strong>, <strong>@build</strong> и <strong>@explore</strong> стандартных режимов агентов мы готовим <strong>@webflow</strong> режим, специально заточенный под Webflow: структуру проекта, взаимодействие с ним и эффективную разработку.</p>",
|
||||
},
|
||||
{
|
||||
question: "Что такое собственная база знаний?",
|
||||
answerHtml:
|
||||
"<p>Основано на многолетнем опыте написания кастомного кода для Webflow-анимаций. </p><p>WebGL, Three.js, GSAP и кастомная функциональность собраны в набор практик, пайплайнов и модульную систему, чтобы всё работало согласованно. </p><p>Также доступна библиотека компонентов: выберите компонент, добавьте в проект и попросите ИИ адаптировать его под задачу.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как использовать это с Webflow?",
|
||||
answerHtml:
|
||||
"<p>При запуске <em>NODE.DC</em> доступна отдельная вкладка для встраивания. </p><p>В зависимости от задачи можно получить скрипты для конкретной страницы или единый app.js для всего проекта. </p><p>Сгенерированная ссылка содержит и продакшен-версию, следующую за URL деплоя, и локальную версию для предпросмотра без влияния на посетителей сайта.</p>",
|
||||
},
|
||||
{
|
||||
question: "Какие ИИ-модели доступны?",
|
||||
answerHtml:
|
||||
"<p>Мы используем основные доступные модели: <strong>OpenAI</strong> до <strong>Claude</strong> до <strong>Gemini</strong>. </p><p>Оркестратор выбирает модель под задачу автоматически, но вы можете вручную переключиться на нужную модель.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как это работает?",
|
||||
answerHtml:
|
||||
"<p>Во время работы NODE.DC выполняет две задачи параллельно.</p><p>Поднимает локальную среду разработки на Bun, синхронизирует JavaScript и TypeScript, включает HMR и держит webhook для автоматического обновления сайта при изменениях. </p><p>Одновременно работает оркестратор ИИ-агентов: обрабатывает запросы, меняет код, проверяет состояние сборки и при необходимости обновляет проект Webflow.</p>",
|
||||
},
|
||||
{
|
||||
question: "Как работает публикация?",
|
||||
answerHtml:
|
||||
"<p>Процесс многоэтапный: сначала Bun собирает, сжимает, минифицирует и подготавливает проект вместе с библиотеками.</p><p>Затем файлы отправляются в CDN: <code>cdn.node.dc/{USERNAME}/{PROJECT-NAME}</code>. Ссылка уже содержит нужную структуру: вы видите локальную версию, а пользователи получают опубликованный скрипт.</p>",
|
||||
},
|
||||
{
|
||||
question: "Это работает с MCP?",
|
||||
answerHtml:
|
||||
"<p>Система изначально рассчитана на работу с Webflow MCP. </p><p>Подключите проект и авторизацию, чтобы ИИ-агенты получили доступ к Webflow-сборке. Подключение приложения в Webflow Designer даёт агентам доступ к контенту, элементам, стилям и CSS, а кастомный код получает полный контекст проекта.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
editableFields: [
|
||||
field("content.heading.muted", "Заголовок: приглушённая строка"),
|
||||
field("content.heading.middle", "Заголовок: средняя строка"),
|
||||
field("content.heading.main", "Заголовок: основная строка"),
|
||||
field("content.introHtml", "Вводный текст", "html"),
|
||||
field("content.docsHref", "Документация: ссылка", "url", "attr"),
|
||||
field("content.docsLabel", "Документация: кнопка"),
|
||||
field("content.windowTitle", "Окно: название"),
|
||||
...Array.from({ length: 10 }, (_, index) => [
|
||||
field(`content.faq.${index}.question`, `FAQ ${index + 1}: вопрос`, "text", "text", "FAQ", {
|
||||
replaceAll: true,
|
||||
}),
|
||||
field(`content.faq.${index}.answerHtml`, `FAQ ${index + 1}: ответ`, "html", "html", "FAQ"),
|
||||
]).flat(),
|
||||
],
|
||||
},
|
||||
"publish-cta": {
|
||||
type: "publishCta",
|
||||
content: {
|
||||
cta: {
|
||||
heading: {
|
||||
muted: "Подключить.",
|
||||
middle: "Поставить задачу.",
|
||||
main: "Опубликовать.",
|
||||
},
|
||||
intro: "Единый путь от разработки до продакшена.",
|
||||
detailHtml:
|
||||
'<span class="darker-70">Настройка примерно в 12 кликов:</span> от скачивания приложения до запуска в проекте.',
|
||||
href: "index.html#hero",
|
||||
buttonLabel: "Встать в лист ожидания",
|
||||
},
|
||||
},
|
||||
editableFields: [
|
||||
field("content.cta.heading.muted", "CTA: приглушённая строка"),
|
||||
field("content.cta.heading.middle", "CTA: средняя строка"),
|
||||
field("content.cta.heading.main", "CTA: основная строка"),
|
||||
field("content.cta.intro", "CTA: вводный текст", "text", "text", "CTA"),
|
||||
field("content.cta.detailHtml", "CTA: пояснение", "html", "html", "CTA"),
|
||||
field("content.cta.href", "CTA: ссылка", "url", "attr", "CTA"),
|
||||
field("content.cta.buttonLabel", "CTA: кнопка", "text", "text", "CTA"),
|
||||
],
|
||||
},
|
||||
"pricing-table": {
|
||||
type: "pricingTable",
|
||||
content: {
|
||||
pricing: {
|
||||
eyebrow: "Тарифы",
|
||||
heading: {
|
||||
muted: "Код",
|
||||
middle: "для No-Code,",
|
||||
main: "усиленный ИИ.",
|
||||
},
|
||||
introHtml: "Подписки на приложение с<br/>включённым объёмом в каждом тарифе. <br/>",
|
||||
tokenBillingHtml: '<span class="darker-70">Токенная</span> оплата сверх лимитов.',
|
||||
founderLimitHtml: '<span class="darker-70">200 Founder</span> мест с фиксированной ценой на первый год.',
|
||||
windowTitle: "Тарифы.app",
|
||||
discountLabel: "Скидка 50%",
|
||||
betaHref: "beta-apply.html",
|
||||
founderHref: "founders.html",
|
||||
betaButtonLabel: "Подать заявку в бету",
|
||||
founderButtonLabel: "Оформить Founder-доступ",
|
||||
basicButtonLabel: "Выбрать Basic",
|
||||
advancedButtonLabel: "Выбрать Advanced",
|
||||
noteHtml: "(*) Условия могут меняться по ходу тестирования, но мы постараемся сохранить заявленные обещания.",
|
||||
enterpriseNoteHtml:
|
||||
'<a href="enterprise.html" class="darker-40">Enterprise</a> BYOK-тарифы будут доступны с сентября 2026.',
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
name: "Бета",
|
||||
priceHtml: "Бесплатно",
|
||||
period: "1 месяц",
|
||||
},
|
||||
{
|
||||
name: "Founder ",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>310',
|
||||
period: "/ год, навсегда",
|
||||
},
|
||||
{
|
||||
name: "Basic",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>20',
|
||||
period: "/ месяц",
|
||||
},
|
||||
{
|
||||
name: "Advanced",
|
||||
priceHtml: '<span class="darker-70 mr-xs lighter">€</span>50',
|
||||
period: "/ месяц",
|
||||
},
|
||||
],
|
||||
},
|
||||
editableFields: [
|
||||
field("content.pricing.eyebrow", "Тарифы: надзаголовок"),
|
||||
field("content.pricing.heading.muted", "Тарифы: приглушённая строка"),
|
||||
field("content.pricing.heading.middle", "Тарифы: средняя строка"),
|
||||
field("content.pricing.heading.main", "Тарифы: основная строка"),
|
||||
field("content.pricing.introHtml", "Тарифы: вводный текст", "html"),
|
||||
field("content.pricing.tokenBillingHtml", "Тарифы: оплата сверх лимитов", "html"),
|
||||
field("content.pricing.founderLimitHtml", "Тарифы: Founder-лимит", "html"),
|
||||
field("content.pricing.windowTitle", "Окно: название"),
|
||||
field("content.pricing.discountLabel", "Founder: бейдж скидки"),
|
||||
field("content.plans.0.name", "Бета: название"),
|
||||
field("content.plans.0.priceHtml", "Бета: цена", "html"),
|
||||
field("content.plans.0.period", "Бета: период"),
|
||||
field("content.plans.1.name", "Founder: название"),
|
||||
field("content.plans.1.priceHtml", "Founder: цена", "html"),
|
||||
field("content.plans.1.period", "Founder: период"),
|
||||
field("content.plans.2.name", "Basic: название"),
|
||||
field("content.plans.2.priceHtml", "Basic: цена", "html"),
|
||||
field("content.plans.2.period", "Basic: период"),
|
||||
field("content.plans.3.name", "Advanced: название"),
|
||||
field("content.plans.3.priceHtml", "Advanced: цена", "html"),
|
||||
field("content.plans.3.period", "Advanced: период"),
|
||||
field("content.pricing.betaHref", "Бета: ссылка", "url", "attr"),
|
||||
field("content.pricing.founderHref", "Founder: ссылка", "url", "attr"),
|
||||
field("content.pricing.betaButtonLabel", "Бета: кнопка"),
|
||||
field("content.pricing.founderButtonLabel", "Founder: кнопка"),
|
||||
field("content.pricing.basicButtonLabel", "Basic: кнопка"),
|
||||
field("content.pricing.advancedButtonLabel", "Advanced: кнопка"),
|
||||
field("content.pricing.noteHtml", "Тарифы: примечание", "html"),
|
||||
field("content.pricing.enterpriseNoteHtml", "Тарифы: Enterprise-примечание", "html"),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const staticElements = {
|
||||
|
|
@ -479,7 +823,7 @@ function tokenizeStaticShell(source, definition) {
|
|||
tokenized = replaceShellLiteral(
|
||||
tokenized,
|
||||
"<script>\nwindow.__API_ORIGIN__=",
|
||||
`<script id="node-dc-static-config" type="application/json">{"assets":{"icon":"${shellToken(
|
||||
`\n<script id="node-dc-static-config" type="application/json">{"assets":{"icon":"${shellToken(
|
||||
"js",
|
||||
"content.assets.webglModelSrc",
|
||||
)}","logo":"${shellToken("js", "content.assets.logoPng")}"}}</script>\n<script>\nwindow.__API_ORIGIN__=`,
|
||||
|
|
|
|||
Loading…
Reference in New Issue