/
deka
/
buffer
Обзор
Документация
Войти
/
deka
/
buffer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
dev
lab/wiki-clipboard-lab.template.html
5 653 строки
210 KB
Maksim Ratnikov
fix: доска отвергала вставку из-за дробной координаты
10 часов назад
10 часов назад
51ca8ed
Код
Авторство
О чём код?
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Wiki Clipboard Lab</title> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23157a4b'/%3E%3Ctext x='16' y='21' text-anchor='middle' font-family='Arial, sans-serif' font-size='13' font-weight='700' fill='white'%3EWL%3C/text%3E%3C/svg%3E"> <style> /* __INJECT_FONTS__ */ :root { color-scheme: light; /* Colour */ --bg: #f4f6f8; --panel: #ffffff; --panel-sunken: #f8fafb; --text: #171a1f; --text-strong: #111419; --muted: #69717d; --line: #dfe4e9; --line-strong: #c9d0d8; --accent: #157a4b; --accent-strong: #0e5d39; --accent-soft: #e9f6ef; --on-accent: #ffffff; --ok: #117025; --warn: #ad5800; --warn-soft: #fdecc8; --bad: #b91c1c; --bad-soft: #fbe2e2; /* Focus + shadow */ --focus-ring: 0 0 0 3px rgba(21, 122, 75, 0.18); --focus-ring-inset: inset 0 0 0 3px rgba(21, 122, 75, 0.18); --shadow: 0 12px 30px rgba(23, 26, 31, 0.10); /* Typography */ --font-sans: 'IBM Plex Sans', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; --font-mono: 'IBM Plex Mono', "SFMono-Regular", "Cascadia Mono", Consolas, "Liberation Mono", monospace; --fs-xs: 11px; --fs-sm: 12px; --fs-base: 13px; --fs-md: 14px; --fs-lg: 16px; --lh-tight: 1.3; --lh-base: 1.5; --ls-label: 0; /* Spacing + shape */ --radius: 6px; --panel-radius: 6px; --btn-radius: 6px; --gap-tight: 6px; --gap: 12px; --gap-loose: 24px; --btn-h: 34px; --header-h: 68px; --statusbar-h: 38px; --sidebar-w: 232px; /* Wiki-block surfaces (set in Task 14) */ --wiki-info-bg: #e8eef8; --wiki-info-border: #b1c3df; --wiki-info-text: #14385f; --wiki-warning-bg: #fdecc8; --wiki-warning-border: #e3b46d; --wiki-warning-text: #7a4b00; --wiki-tip-bg: #e2f0e6; --wiki-tip-border: #95c2a4; --wiki-tip-text: #1d5732; --wiki-hint-bg: #fff4c2; --wiki-hint-text: #604700; --wiki-table-bg: #ffffff; --wiki-table-border: #c8d1dd; --wiki-table-head-bg: #e6ebf1; /* Legacy aliases — kept ONLY while specific surfaces still reference them. Each is removed in the task that migrates the consumer. */ --link: var(--accent-strong); --btn-bg-hover: var(--panel-sunken); --accent-soft-bg: var(--accent-soft); --accent-soft-bg-hover: #d6e8db; --accent-soft-border: var(--accent); --accent-soft-border-hover: var(--accent-strong); --warn-btn-bg: var(--warn-soft); --warn-btn-bg-hover: #fbdfa5; --warn-btn-border: var(--warn); --warn-btn-border-hover: #8a4500; --warn-btn-text: var(--warn); --warn-btn-text-hover: #6a3700; --ok-bg: rgba(17, 112, 37, 0.08); --warn-bg: rgba(173, 88, 0, 0.08); --bad-bg: rgba(185, 28, 28, 0.08); --ok-border: rgba(17, 112, 37, 0.35); --warn-border: rgba(173, 88, 0, 0.35); --bad-border: rgba(185, 28, 28, 0.35); --overlay: rgba(14, 22, 34, 0.45); --wiki-chrome-bg: var(--panel-sunken); --wiki-chrome-border: var(--line); --wiki-chrome-border-soft: var(--line); --wiki-chrome-text: var(--text); --wiki-chrome-active-border: var(--accent); --wiki-preview-tab-bg: var(--panel); --wiki-preview-tab-text: var(--muted); --wiki-preview-tab-hover-text: var(--text); --wiki-preview-tab-active-text: var(--accent-strong); --tab-font-size: var(--fs-base); --placeholder-font-size: var(--tab-font-size); --panel-gap: var(--gap); --panel-padding: var(--gap); --panel-title-height: 38px; font-family: var(--font-sans); } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); min-height: 100vh; } button, input, textarea { font: inherit; } button { border: 1px solid var(--line); background: var(--panel); color: var(--text); min-height: var(--btn-h); padding: 0 11px; border-radius: var(--btn-radius); cursor: pointer; font-family: var(--font-sans); font-size: var(--fs-sm); font-weight: 500; letter-spacing: 0; } button:hover { border-color: var(--line-strong); background: var(--btn-bg-hover); } button.btn-primary { border-color: var(--accent); background: var(--accent); color: var(--on-accent); } button.btn-primary:hover { border-color: var(--accent-strong); background: var(--accent-strong); } .btn-soft { border: 1px solid var(--accent); background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; min-height: var(--btn-h); padding: 0 11px; border-radius: var(--btn-radius); font-family: var(--font-sans); font-size: var(--fs-sm); cursor: pointer; } .btn-soft:hover { border-color: var(--accent-strong); background: #d6e8db; } .btn-warning { border: 1px solid var(--warn); background: var(--warn-soft); color: var(--warn); font-weight: 600; min-height: var(--btn-h); padding: 0 11px; border-radius: var(--btn-radius); font-family: var(--font-sans); font-size: var(--fs-sm); cursor: pointer; } .btn-warning:hover { border-color: #8a4500; background: #fbdfa5; } .btn-external { display: inline-flex; align-items: center; gap: 6px; min-height: var(--btn-h); padding: 0 11px; border: 1px solid transparent; background: transparent; color: var(--muted); font-family: var(--font-sans); font-size: var(--fs-sm); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line-strong); cursor: pointer; } .btn-external:hover { color: var(--accent-strong); text-decoration-color: var(--accent); } .btn-external:has(.llm-ico) { text-decoration: none; } .llm-ico { width: 15px; height: 15px; flex: none; border-radius: 4px; } .llm-launchers { display: inline-flex; align-items: center; gap: 8px; flex-wrap: nowrap; } .toolbar .btn-clear { margin-left: auto; } button:disabled { cursor: not-allowed; border-color: var(--line); background: var(--panel-sunken); color: var(--muted); box-shadow: none; } .btn-external:disabled { cursor: not-allowed; border-color: transparent; background: transparent; color: var(--muted); text-decoration-color: var(--line); } .btn-flash-ok { background: var(--accent) !important; color: var(--on-accent) !important; border-color: var(--accent-strong) !important; transition: background 150ms; } .external-group-label { align-self: center; color: var(--muted); font-family: var(--font-sans); font-size: var(--fs-xs); letter-spacing: var(--ls-label); text-transform: lowercase; padding-right: 4px; } .contact-link { display: inline-flex; align-items: center; color: var(--muted); font-family: var(--font-sans); font-size: var(--fs-sm); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line-strong); } .contact-link:hover { color: var(--accent-strong); text-decoration-color: var(--accent); } .release-meta { display: flex; flex: none; flex-direction: column; align-items: flex-end; gap: 4px; margin-left: auto; } .version-meta { display: inline-flex; align-items: center; gap: 6px; color: var(--text); font-family: var(--font-mono); font-size: var(--fs-sm); line-height: var(--lh-tight); white-space: nowrap; } .version-number { padding: 2px 6px; border: 1px solid var(--accent); border-radius: var(--btn-radius); background: var(--accent-soft); color: var(--accent-strong); font-weight: 700; } .version-date { font-weight: 600; } .prompt-modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; } .prompt-modal[hidden] { display: none !important; } .prompt-modal-backdrop { position: absolute; inset: 0; background: var(--overlay); } .prompt-modal-panel { position: relative; max-width: 720px; width: calc(100vw - 32px); max-height: calc(100vh - 64px); background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px 18px; display: flex; flex-direction: column; gap: 10px; } .prompt-modal-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .prompt-modal-header h2 { margin: 0; font-family: var(--font-sans); font-size: var(--fs-md); font-weight: 600; color: var(--accent-strong); letter-spacing: 0; } .prompt-modal-close { width: 26px; height: 26px; min-height: 26px; padding: 0; border-radius: 999px; font-size: var(--fs-md); line-height: 1; } .prompt-modal-subtitle { margin: 0; color: var(--muted); font-size: var(--fs-sm); } .prompt-modal-label { margin: 2px 0 -4px; font-family: var(--font-sans); font-size: var(--fs-xs); font-weight: 600; color: var(--muted); } .prompt-modal-source { flex: 1 1 auto; min-height: 160px; resize: vertical; padding: 10px 12px; background: var(--panel-sunken); border: 1px solid var(--line); border-radius: var(--radius); font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-strong); } .prompt-modal-toolbar { min-height: 0; padding: 0 0 10px; border-top: 0; border-bottom: 1px solid var(--line); background: transparent; } .llm-open-group { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; } .prompt-modal-body { flex: 1; min-height: 0; margin: 0; padding: 10px 12px; background: var(--panel-sunken); border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; font-family: var(--font-mono); font-size: var(--fs-xs); white-space: pre-wrap; word-break: break-word; color: var(--text-strong); } .prompt-modal-footer { display: flex; justify-content: flex-end; align-items: center; flex-wrap: wrap; gap: 8px; } textarea::-webkit-scrollbar, .editor::-webkit-scrollbar, .layout-output::-webkit-scrollbar, .paste-zone::-webkit-scrollbar, .excel-paste-zone::-webkit-scrollbar, .prompt-modal-body::-webkit-scrollbar { width: 10px; height: 10px; } textarea::-webkit-scrollbar-track, .editor::-webkit-scrollbar-track, .layout-output::-webkit-scrollbar-track, .paste-zone::-webkit-scrollbar-track, .excel-paste-zone::-webkit-scrollbar-track, .prompt-modal-body::-webkit-scrollbar-track { background: var(--panel-sunken); } textarea::-webkit-scrollbar-thumb, .editor::-webkit-scrollbar-thumb, .layout-output::-webkit-scrollbar-thumb, .paste-zone::-webkit-scrollbar-thumb, .excel-paste-zone::-webkit-scrollbar-thumb, .prompt-modal-body::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; } textarea::-webkit-scrollbar-thumb:hover, .editor::-webkit-scrollbar-thumb:hover, .layout-output::-webkit-scrollbar-thumb:hover, .paste-zone::-webkit-scrollbar-thumb:hover, .excel-paste-zone::-webkit-scrollbar-thumb:hover, .prompt-modal-body::-webkit-scrollbar-thumb:hover { background: var(--muted); } .app { display: grid; grid-template-rows: auto auto 1fr; min-height: 100vh; } header { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 14px; padding: 8px 18px; border-bottom: 1px solid var(--line); background: var(--panel); position: sticky; top: 0; z-index: 10; min-height: var(--header-h); } .brand { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex: none; border-radius: var(--btn-radius); background: var(--accent); color: var(--on-accent); font-family: var(--font-mono); font-weight: 700; font-size: 13px; letter-spacing: 0; user-select: none; } .tool-tabs-label { color: var(--accent-strong); font-family: var(--font-sans); font-size: var(--fs-base); font-weight: 600; line-height: var(--lh-tight); letter-spacing: 0; white-space: nowrap; } .status-bar { position: sticky; top: var(--header-h); z-index: 9; display: flex; align-items: center; gap: 14px; height: var(--statusbar-h); padding: 4px 18px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); border-left: 3px solid var(--line-strong); font-family: var(--font-mono); font-size: var(--fs-sm); font-weight: 500; background: var(--panel-sunken); color: var(--muted); white-space: nowrap; overflow-x: auto; } .status-bar .status-prefix { letter-spacing: var(--ls-label); flex: none; } .status-bar .status-message { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } .status-bar[data-state="ok"] { background: var(--accent-soft); color: var(--accent-strong); border-left-color: var(--accent); } .status-bar[data-state="warn"] { background: var(--warn-soft); color: var(--warn); border-left-color: var(--warn); } .status-bar[data-state="bad"] { background: var(--bad-soft); color: var(--bad); border-left-color: var(--bad); } .toolbar { display: flex; flex-wrap: wrap; gap: 8px; padding: 10px 18px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--panel-sunken); } .toolbar .spacer { flex: 1 1 auto; margin-left: auto; } .goal-diff-panel .goal-diff-actions { display: flex; gap: 8px; margin: 10px 0; } .goal-diff-report { max-height: 320px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); } .goal-diff-report:empty { display: none; } .goal-diff-report table { width: 100%; border-collapse: collapse; font-size: var(--fs-xs); } .goal-diff-report th, .goal-diff-report td { padding: 5px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } .goal-diff-report th { position: sticky; top: 0; background: var(--panel-sunken); } .goal-diff-report tr[data-kind="removed"] { background: #fff0f0; } .goal-diff-report tr[data-kind="added"] { background: #f2fbf5; } .goal-diff-summary { padding: 8px 10px; border-bottom: 1px solid var(--line); font-family: var(--font-mono); font-size: var(--fs-xs); } .panel-title-note { margin-left: auto; max-width: 60%; color: var(--muted); font-size: var(--fs-xs); text-align: right; } .toolbar-field { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-xs); } .toolbar-field select { padding: 5px 8px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); color: var(--text); font-family: var(--font-mono); font-size: var(--fs-xs); } .meet-prompt-modes { flex: none; } .meet-prompt-modes button { white-space: nowrap; } .tool-tabs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-tight); flex: 1 1 auto; border-radius: var(--btn-radius); padding: 6px; background: var(--panel-sunken); border: 1px solid var(--line); } .tab-group { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap-tight); } .tool-tabs button { min-height: 32px; padding: 0 11px; border: 1px solid var(--line); border-radius: var(--btn-radius); background: var(--panel); color: var(--muted); font-family: var(--font-sans); font-weight: 500; font-size: var(--tab-font-size); display: inline-flex; align-items: center; gap: 8px; cursor: pointer; } .tool-tabs button:hover { border-color: var(--line-strong); color: var(--text); } .tool-tabs button[aria-selected="true"] { background: var(--accent-soft); border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); color: var(--accent-strong); font-weight: 600; } .tool-tabs .tab-info { width: 18px; height: 18px; flex: none; border: 1px solid transparent; background: rgba(86, 101, 122, 0.12); color: var(--muted); font-weight: 600; font-style: italic; font-family: ui-serif, Georgia, "Times New Roman", serif; font-size: 12px; border-radius: 999px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; } .tool-tabs .tab-info:hover { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); } .tool-tabs button[aria-expanded="true"] .tab-info { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); } .popover { position: fixed; max-width: 320px; padding: 12px 14px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); color: var(--text); z-index: 50; font-family: var(--font-sans); } .popover-title { margin: 0 0 6px 0; font-size: var(--fs-base); font-family: var(--font-sans); font-weight: 600; letter-spacing: 0; color: var(--accent-strong); } .popover-body { margin: 0; font-size: var(--fs-sm); line-height: var(--lh-base); color: var(--text); } .tools { padding: 12px; min-height: 0; } .tool-view { min-height: 0; } .tool-view[hidden] { display: none !important; } .clipboard-layout { display: grid; grid-template-columns: minmax(280px, 380px) minmax(0, 1fr); min-height: 0; } .tool-workspace, .clipboard-layout { gap: var(--panel-gap); padding-top: var(--panel-gap); } .clipboard-layout > aside, .clipboard-layout > section, .tool-workspace > section { min-height: 0; } .panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--panel-radius); box-shadow: var(--shadow); min-height: 0; overflow: hidden; } .panel + .panel { margin-top: var(--panel-gap); } .tool-workspace > .panel + .panel, .raw-grid > .panel + .panel { margin-top: 0; } .panel-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--panel-sunken); min-height: var(--panel-title-height); } .panel-title-link { flex: none; min-height: 26px; padding: 0 2px; border: 0; background: transparent; color: var(--muted); font-size: var(--fs-sm); font-weight: 500; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: var(--line-strong); cursor: pointer; } .panel-title-link:hover { border: 0; background: transparent; color: var(--accent-strong); text-decoration-color: var(--accent); } h2 { font-size: var(--fs-base); line-height: var(--lh-tight); margin: 0; font-weight: 500; font-family: var(--font-sans); letter-spacing: 0; color: var(--text-strong); } .raw-body { height: calc(100% - 45px); min-height: 220px; } .full-body, .layout-output, .panel-body, .raw-body { padding: var(--panel-padding); } .paste-zone, .excel-paste-zone { border: 1px dashed #8aa8c6; border-radius: var(--panel-radius); padding: var(--panel-padding); background: #f8fbff; color: var(--muted); font-size: var(--placeholder-font-size); line-height: 1.45; outline: none; overflow: auto; } .paste-zone { min-height: 138px; } .paste-target { position: relative; display: grid; min-height: 0; height: 100%; } .paste-target > textarea, .paste-target > .paste-zone, .paste-target > .excel-paste-zone { grid-area: 1 / 1; } .paste-target-overlay { grid-area: 1 / 1; z-index: 1; align-self: start; padding: 18px; pointer-events: none; color: var(--muted); font-family: var(--font-sans); line-height: var(--lh-base); } .paste-target-prompt { margin-bottom: 8px; color: var(--accent); font-size: 24px; font-weight: 400; font-style: italic; line-height: var(--lh-tight); letter-spacing: 0; white-space: nowrap; } .paste-target-helper { max-width: 62ch; color: var(--muted); font-size: var(--placeholder-font-size); line-height: 1.45; } .paste-target:focus-within .paste-target-overlay { display: none; } .paste-target > textarea:not(:placeholder-shown) ~ .paste-target-overlay, .paste-target > .paste-zone:not(:empty) ~ .paste-target-overlay, .paste-target > .excel-paste-zone:not(:empty) ~ .paste-target-overlay { display: none; } .paste-zone:empty::before, .excel-paste-zone:empty::before { content: attr(data-placeholder); color: var(--muted); white-space: pre-wrap; pointer-events: none; } .paste-zone:focus:empty::before, .excel-paste-zone:focus:empty::before { content: ""; } .excel-paste-zone { min-height: 96px; } .paste-zone:focus, .excel-paste-zone:focus, textarea:focus { border-color: var(--accent); box-shadow: var(--focus-ring); } textarea { font-family: var(--font-mono); font-size: var(--fs-sm); line-height: var(--lh-base); } textarea::placeholder { color: var(--muted); font-size: var(--placeholder-font-size); font-family: var(--font-sans); line-height: 1.45; opacity: 1; } textarea:focus::placeholder { color: transparent; } .types-list { display: flex; flex-wrap: wrap; gap: 6px; min-height: 31px; } .type-pill { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border-radius: 3px; background: var(--panel-sunken); border: 1px solid var(--line); color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1; white-space: nowrap; } .type-pill.wiki { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-strong); } .mini-label { margin: 12px 0 6px; color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-xs); font-weight: 600; letter-spacing: var(--ls-label); text-transform: uppercase; } .meta-grid { display: grid; grid-template-columns: 96px minmax(0, 1fr); gap: 8px; margin-top: 10px; color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-sm); } .meta-grid code { color: var(--text); overflow-wrap: anywhere; font-family: var(--font-mono); } .workspace { display: grid; grid-template-rows: minmax(320px, 1fr) minmax(320px, 42vh); gap: 12px; min-height: 0; } .editor-panel { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .editor { min-height: 0; height: 100%; padding: 16px; overflow: auto; outline: none; background: #fff; line-height: 1.55; } .preview-empty { display: flex; flex-direction: column; align-items: stretch; gap: 16px; padding: 18px; pointer-events: none; user-select: none; } .preview-empty[hidden] { display: none !important; } .preview-skeleton { display: grid; gap: 10px; } .preview-skeleton > div { background: var(--line); opacity: 0.30; border-radius: 2px; } .preview-skeleton .sk-heading { height: 22px; width: 50%; } .preview-skeleton .sk-paragraph { height: 10px; } .preview-skeleton .sk-paragraph-1 { width: 92%; } .preview-skeleton .sk-paragraph-2 { width: 78%; } .preview-skeleton .sk-paragraph-3 { width: 85%; } .preview-skeleton .sk-table { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); opacity: 0.30; margin-top: 6px; padding: 1px; border-radius: 2px; } .preview-skeleton .sk-table > div { height: 24px; background: var(--panel); opacity: 1; } .preview-skeleton .sk-buttons { display: flex; gap: 8px; margin-top: 6px; } .preview-skeleton .sk-buttons > div { width: 80px; height: 28px; border-radius: var(--btn-radius); } .preview-empty-hint { margin: 6px 0 0; font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--muted); text-align: center; } .prompt-output-shell { position: relative; display: grid; min-height: 0; } .prompt-output-shell > textarea, .prompt-output-empty { grid-area: 1 / 1; } .prompt-output-empty { z-index: 1; align-self: start; margin: 0; background: var(--panel-sunken); border-radius: var(--radius); } .prompt-output-shell > textarea:disabled { cursor: default; opacity: 1; } #htmlEditor { min-height: 0; resize: none; border: 0; border-radius: 0; } .editor:focus { box-shadow: var(--focus-ring-inset); } .editor h1, .editor h2, .editor h3, .editor h4, .editor h5, .editor h6 { margin: 18px 0 10px; line-height: 1.25; color: #162336; } .editor h1 { font-size: 26px; } .editor h2 { font-size: 21px; padding-bottom: 5px; border-bottom: 1px solid #d9e3ee; } .editor h3 { font-size: 17px; } .editor p { margin: 7px 0; } .editor .tiptap.ProseMirror { padding: 4px; } .editor [data-type="tableOfContent"], .editor .node-tableOfContent .toctainer_1lfut, .editor .toctainer_geoc4 { display: block; margin: 12px 0; padding: 12px; border: 1px solid #c8d8e8; border-radius: 8px; background: #f4f9ff; color: #35506c; } .editor [data-type="tableOfContent"]::before, .editor .node-tableOfContent .toctainer_1lfut::before, .editor .toctainer_geoc4::before { content: "Содержание"; display: block; margin-bottom: 4px; color: #173f68; font-weight: 700; } .editor [data-type="info"], .editor .node-info .container_15vtd { display: block; margin: 12px 0; padding: 12px 14px; border: 1px solid var(--wiki-info-border); border-left: 4px solid var(--wiki-info-border); border-radius: var(--radius); background: var(--wiki-info-bg); color: var(--wiki-info-text); } .editor [data-type="info"][title]::before { content: attr(title); display: block; margin: -2px 0 8px; color: #173f68; font-weight: 700; line-height: 1.25; } .editor [data-type="info"][showtitle="false"]::before, .editor [data-type="info"][title=""]::before { display: none; } /* A wiki panel starts with a text descriptor div followed by the content div. The descriptor is part of the paste contract, not something to show here. */ .editor [data-type="info"] > div:first-child:not(:last-child) { display: none; } .editor .node-info .header_15vtd, .editor .node-info .title_15vtd { display: block; margin-bottom: 8px; color: #173f68; font-weight: 700; line-height: 1.25; } .editor [data-type="info"][type="warning"], .editor .node-info .warning-0-2-331 { border-left-color: var(--wiki-warning-border); background: var(--wiki-warning-bg); color: var(--wiki-warning-text); } .editor [data-type="info"][type="tip"] { border-left-color: var(--wiki-tip-border); background: var(--wiki-tip-bg); color: var(--wiki-tip-text); } .editor [data-type="status"], .editor .node-status [class*="status-"] { display: inline-flex; align-items: center; max-width: 100%; min-height: 22px; margin: 0 3px 2px 0; padding: 2px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel-sunken); color: var(--muted); font-family: var(--font-mono); font-size: var(--fs-xs); line-height: var(--lh-tight); white-space: nowrap; vertical-align: middle; } .editor [data-type="status"][color="green"] { border-color: #9bd0b2; background: #e8f7ee; color: #146c43; } .editor [data-type="status"][color="yellow"], .editor [data-type="status"][color="orange"] { border-color: #e3c078; background: #fff7df; color: #7a4b00; } .editor [data-type="status"][color="red"] { border-color: #e2a5a5; background: #fff0f0; color: #9b2c2c; } .editor [data-type="status"][color="blue"], .editor [data-type="status"][color="light-blue"] { border-color: #a8c8ea; background: #eef7ff; color: #165a9f; } .editor [data-type="status"][color="purple"] { border-color: #c8b4e8; background: #f6f0ff; color: #5d3b8c; } .editor [data-type="status"][color="black"], .editor [data-type="status"][color="gray"] { border-color: #c8ced6; background: #f0f2f4; color: #3e4854; } .editor [data-type="uiButton"], .editor .node-uiButton { display: inline-flex; max-width: 100%; margin: 5px 8px 8px 0; vertical-align: middle; } .editor [data-type="uiButton"] button, .editor .node-uiButton button { min-height: 30px; border-radius: var(--btn-radius); border-color: var(--line-strong); background: var(--panel); color: var(--text); box-shadow: 0 1px 2px rgba(14, 22, 34, 0.05); font-family: var(--font-sans); font-size: var(--fs-sm); } .editor [data-type="uiButton"][display="block"] { display: flex; } .editor [data-type="uiButton"][display="block"] button { width: 100%; justify-content: center; } .editor [data-type="uiButton"][color="Foreground.forePositive"] button { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); } .editor [data-type="uiButton"][color="Foreground.foreAccent"] button, .editor [data-type="uiButton"][color="sky.sky500"] button { border-color: var(--wiki-info-border); background: var(--wiki-info-bg); color: var(--wiki-info-text); } .editor [data-type="uiButton"][color="Foreground.foreWarning"] button { border-color: var(--wiki-warning-border); background: var(--wiki-warning-bg); color: var(--wiki-warning-text); } .editor [data-type="uiButton"][color="Foreground.foreNegative"] button { border-color: #e6a5a5; background: var(--bad-soft); color: var(--bad); } .editor [data-type="table"], .editor .node-table { display: block; max-width: 100%; margin: 12px 0; overflow: auto; border: 1px solid var(--wiki-table-border); border-radius: var(--radius); background: var(--wiki-table-bg); } .editor [data-type="table"] table, .editor .node-table table { width: max-content; min-width: 100%; border-collapse: collapse; } .editor [data-type="table"] th, .editor [data-type="table"] td, .editor .node-table th, .editor .node-table td { min-width: 120px; padding: 7px 9px; border: 1px solid var(--wiki-table-border); vertical-align: top; } .editor [data-type="table"] th, .editor .node-table th { background: var(--wiki-table-head-bg); color: var(--text-strong); font-weight: 600; } .editor [data-type="blocksGrid"], .editor .grid_1orlh { display: grid; grid-template-columns: var(--blocksgrid-template, repeat(2, minmax(0, 1fr))); gap: 12px; max-width: 100%; margin: 12px 0; padding: 0; } .editor [data-type="column"], .editor .column_yu7m7, .editor .column_kzy3y { min-width: 0; padding: 10px; border: 1px dashed var(--wiki-chrome-border-soft); border-radius: 8px; background: #fbfdff; } .editor [data-type="taskList"] { margin: 10px 0; padding-left: 0; list-style: none; } .editor [data-type="taskItem"], .editor .node-taskItem { margin: 6px 0; } .editor .orderedList_14jqc { margin: 10px 0; padding-left: 28px; } .editor .listItem_14jqc { margin: 5px 0; } .editor [data-type="hint"], .editor .hint_jjms4 { display: inline; padding: 1px 5px; border-radius: 4px; background: var(--wiki-hint-bg); color: var(--wiki-hint-text); box-decoration-break: clone; } .editor [data-type="include"], .editor .node-include [data-type="include"] { display: block; margin: 12px 0; padding: 12px; border: 1px dashed #9fbbd8; border-radius: 8px; background: #f4f9ff; color: #35506c; } .editor [data-type="include"]::before, .editor .node-include [data-type="include"]::before { content: "Include"; display: inline-flex; margin-right: 8px; padding: 2px 7px; border-radius: 999px; background: #deecf9; color: #173f68; font-size: 12px; font-weight: 700; } .editor [data-type="plantuml"], .editor .plantContainer_1hv20 { display: block; margin: 12px 0; padding: 12px; border: 1px solid #cbd9e7; border-radius: 8px; background: #fbfcfd; } .editor .codeToolbar_rfjdv, .editor .imageContainer_1hv20, .editor .imageToolbar_w55d3 { display: none; } .editor [data-type="plantuml"] pre, .editor .plantContainer_1hv20 pre, .editor pre[data-set="highlighted-code"] { margin: 0; overflow: auto; padding: 10px; border-radius: 6px; background: #1f2937; color: #edf2f7; white-space: pre; } .editor [data-type="tabs"], .editor .tabsWrapper_d6kqf { display: block; margin: 12px 0; border: 1px solid var(--wiki-chrome-border); border-radius: 8px; background: #fff; overflow: hidden; } .editor .tabHeader_d6kqf { display: flex; align-items: center; gap: 6px; padding: 8px 10px; background: var(--wiki-chrome-bg); border-bottom: 1px solid var(--wiki-chrome-border); } .editor .leftBtnContainer_d6kqf, .editor .rightBtnContainer_d6kqf, .editor .tab-handler, .editor .removeNodeBtn_d6kqf, .editor .tabLabel_1f49m > button { display: none !important; } .editor .tabLabelContainer_d6kqf { display: flex; flex-wrap: wrap; gap: 6px; } .editor .tabLabel_1f49m { display: inline-flex; align-items: center; min-height: 28px; padding: 0 8px; border: 1px solid var(--wiki-chrome-border-soft); border-radius: 6px; background: #fff; } .editor .tabLabel_1f49m.active_1f49m { border-color: var(--wiki-chrome-active-border); background: #e7f2ff; } .editor .tabLabelInput_1f49m { width: auto; min-width: 72px; border: 0; outline: 0; background: transparent; color: var(--wiki-chrome-text); font: inherit; font-weight: 600; } .editor [data-type="tab"], .editor .tabWrapper_z4u1j, .editor .tabContainer_z4u1j { display: block; border-top: 1px solid #e2e8f0; } .editor .tabTitle_z4u1j { display: block; padding: 8px 10px; background: var(--wiki-chrome-bg); color: var(--wiki-chrome-text); font-weight: 700; } .layout-preview .preview-tab-bar { display: flex; flex-wrap: wrap; gap: 2px; padding: 6px 8px 0; background: var(--wiki-chrome-bg); border-bottom: 1px solid var(--wiki-chrome-border); } .layout-preview .preview-tab-bar button { appearance: none; border: 1px solid var(--wiki-chrome-border-soft); border-bottom: 0; background: var(--wiki-preview-tab-bg); color: var(--wiki-preview-tab-text); padding: 6px 12px; border-top-left-radius: 6px; border-top-right-radius: 6px; font: inherit; font-weight: 600; cursor: pointer; margin-bottom: -1px; } .layout-preview .preview-tab-bar button:hover { background: #fff; color: var(--wiki-preview-tab-hover-text); } .layout-preview .preview-tab-bar button[aria-selected="true"] { background: #fff; color: var(--wiki-preview-tab-active-text); border-color: var(--wiki-chrome-active-border); box-shadow: 0 -1px 0 #fff inset; } .layout-preview .tabWrapper_z4u1j > .tabTitle_z4u1j { display: none; } .layout-preview .tabWrapper_z4u1j[data-preview-active="false"] > div:not(.tabTitle_z4u1j) { display: none; } .editor .expand, .editor [data-type="expand"] { display: block; margin: 12px 0; border: 1px solid var(--wiki-chrome-border); border-radius: 8px; background: #fff; overflow: hidden; } .editor .expand > button { display: none; } .editor .expand-summary { display: block; padding: 10px 12px; background: var(--wiki-chrome-bg); color: var(--wiki-chrome-text); font-weight: 700; } .editor .expand-content { display: block !important; padding: 10px 12px; } .editor .expand-content[hidden] { opacity: 0.75; } .editor [data-analytics], .editor [data-testid*="DeleteNode"], .editor [data-testid*="CopyCode"], .editor [data-testid*="CollapseCode"], .editor [data-testid*="OpenNode"], .editor [data-testid*="Button.Arrow"], .editor [data-testid*="Button.Close"], .editor [data-testid*="Button.DragIndicator"] { display: none !important; } .raw-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; height: 100%; min-height: 0; } textarea { width: 100%; height: 100%; min-width: 260px; min-height: 260px; resize: both; border: 1px solid var(--line); border-radius: 8px; padding: 10px; color: var(--text); background: var(--panel-sunken); line-height: 1.45; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 12px; outline: none; tab-size: 2; } .tabs { display: inline-flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; background: var(--panel); } .tabs button { border: 0; border-radius: 0; min-height: 30px; padding: 0 10px; } .tabs button[aria-pressed="true"] { background: #deecf9; color: #063f78; } .tool-workspace { display: grid; grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr); min-height: calc(100vh - 140px); } .kb-workspace { display: grid; grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr); gap: var(--panel-gap); padding-top: var(--panel-gap); min-height: calc(100vh - 140px); } .kb-left { display: grid; grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); gap: var(--panel-gap); min-height: 0; } .kb-workspace > .panel, .kb-left > .panel { min-height: 0; } @media (max-width: 1100px) { .kb-workspace { grid-template-columns: 1fr; min-height: calc(100vh - 200px); } .kb-left { grid-template-rows: minmax(180px, 1fr) minmax(180px, 1fr); } } .work-panel { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; } .full-body { display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 10px; min-height: 0; } .action-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; } .large-textarea { min-height: 0; resize: none; } .result-meta { min-height: 24px; color: var(--muted); font-size: 13px; overflow-wrap: anywhere; } .layout-output { display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 10px; min-height: 0; } .layout-preview { min-height: 0; border: 1px solid var(--line); border-radius: 8px; } .hidden { display: none !important; } @media (max-width: 980px) { .status { max-width: 100%; } .clipboard-layout, .tool-workspace { grid-template-columns: 1fr; } .workspace { grid-template-rows: minmax(260px, 48vh) minmax(320px, auto); } .raw-grid { grid-template-columns: 1fr; } } /* Modern application shell */ body { overflow: hidden; font-size: var(--fs-md); } .app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); grid-template-rows: var(--header-h) minmax(0, 1fr) var(--statusbar-h); width: 100%; height: 100vh; min-height: 0; overflow: hidden; } header { position: static; z-index: 10; grid-column: 1; grid-row: 1 / -1; display: flex; min-width: 0; min-height: 0; padding: 0; flex-direction: column; align-items: stretch; flex-wrap: nowrap; gap: 0; border-right: 1px solid var(--line); border-bottom: 0; background: #fbfcfd; } .brand-row { display: flex; min-height: var(--header-h); align-items: center; gap: 10px; padding: 0 18px; border-bottom: 1px solid var(--line); } .brand { width: 32px; height: 32px; border-radius: 5px; font-size: 13px; letter-spacing: 0; } .brand-copy { display: flex; min-width: 0; flex-direction: column; line-height: var(--lh-tight); } .brand-copy strong { font-size: 15px; font-weight: 700; } .brand-copy span { margin-top: 2px; color: var(--muted); font-size: var(--fs-xs); } .tool-tabs { display: block; min-height: 0; padding: 18px 12px; flex: 1 1 auto; overflow-y: auto; border: 0; border-radius: 0; background: transparent; } .tab-group { display: flex; align-items: stretch; flex-direction: column; gap: 2px; } .tab-group + .tab-group { margin-top: 24px; } .nav-group-label { margin: 0 8px 7px; color: #8b929c; font-size: var(--fs-xs); font-weight: 700; line-height: var(--lh-tight); text-transform: uppercase; } .tool-tabs button { position: relative; display: flex; width: 100%; min-height: 38px; justify-content: space-between; padding: 0 10px 0 13px; border-color: transparent; background: transparent; color: #4e5661; text-align: left; transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease; } .tool-tabs button:hover { border-color: transparent; background: #f0f3f5; } .tool-tabs button[aria-selected="true"] { border-color: transparent; background: var(--accent-soft); box-shadow: none; color: var(--accent-strong); font-weight: 700; } .tool-tabs button[aria-selected="true"]::before { position: absolute; top: 8px; bottom: 8px; left: 0; width: 3px; border-radius: 2px; background: var(--accent); content: ""; } .tool-tabs .tab-info { width: 17px; height: 17px; background: #e9edf1; font-size: 11px; } .release-meta { align-items: flex-start; gap: 8px; margin: 0; padding: 14px 18px 18px; border-top: 1px solid var(--line); } .version-meta { font-size: var(--fs-xs); } .version-number { border-color: #79b894; border-radius: 4px; } .version-date { color: var(--muted); } .app-topbar { grid-column: 2; grid-row: 1; display: flex; min-width: 0; align-items: center; justify-content: space-between; gap: 20px; padding: 0 24px; border-bottom: 1px solid var(--line); background: var(--panel); } .active-tool-heading { min-width: 0; } .active-tool-heading > span { display: block; color: var(--muted); font-size: var(--fs-xs); font-weight: 600; line-height: var(--lh-tight); } .active-tool-heading h1 { margin: 2px 0 0; overflow: hidden; color: var(--text-strong); font-size: 18px; font-weight: 700; line-height: 1.25; text-overflow: ellipsis; white-space: nowrap; } .mobile-topbar-brand { display: none; } .topbar-icon { width: 34px; min-width: 34px; padding: 0; color: #4f5863; font-weight: 700; } button, summary, select { transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease, color 140ms ease, transform 100ms ease; } button:active { transform: translateY(1px); } .mobile-tool-switcher { display: none; } .status-bar { position: static; z-index: 9; grid-column: 2; grid-row: 3; height: var(--statusbar-h); padding: 0 24px; gap: 8px; border-top: 1px solid var(--line); border-right: 0; border-bottom: 0; border-left: 0; background: var(--panel); font-family: var(--font-sans); font-size: var(--fs-sm); } .status-bar .status-prefix { position: relative; padding-left: 15px; font-weight: 700; } .status-bar .status-prefix::before { position: absolute; top: 50%; left: 0; width: 7px; height: 7px; border-radius: 50%; background: var(--line-strong); content: ""; transform: translateY(-50%); transition: background-color 180ms ease, box-shadow 180ms ease; } .status-bar[data-state="ok"], .status-bar[data-state="warn"], .status-bar[data-state="bad"] { background: var(--panel); } .status-bar[data-state="ok"] .status-prefix::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .status-bar[data-state="warn"] .status-prefix::before { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); } .status-bar[data-state="bad"] .status-prefix::before { background: var(--bad); box-shadow: 0 0 0 3px var(--bad-soft); } .tools { grid-column: 2; grid-row: 2; min-width: 0; min-height: 0; padding: 0; overflow: hidden; } .tool-view { display: grid; width: 100%; height: 100%; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; } .tool-view:not([hidden]) { animation: tool-view-enter 160ms ease-out both; } .toolbar { align-items: center; gap: 8px; min-height: 58px; padding: 10px 24px; border-top: 0; border-bottom: 1px solid var(--line); background: var(--panel-sunken); } button, .btn-soft, .btn-warning, .btn-external { min-height: var(--btn-h); border-radius: var(--btn-radius); font-size: var(--fs-base); } .btn-soft { border-color: var(--line-strong); background: var(--panel); color: var(--text); } .btn-soft:hover { border-color: #aab4bf; background: var(--panel-sunken); } .btn-warning { border-color: #e4bf82; background: #fff6e8; color: #96590e; } .btn-external { border: 1px solid transparent; text-decoration: none; } .meet-prompt-modes, .tabs { display: inline-grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); padding: 3px; overflow: visible; border-color: var(--line); border-radius: var(--radius); background: #edf1f4; } .meet-prompt-modes { position: relative; isolation: isolate; width: 350px; max-width: 100%; min-height: 34px; border: 1px solid var(--line-strong); border-radius: 999px; background: #e7ebef; overflow: hidden; } .meet-prompt-modes::before { position: absolute; z-index: 0; top: 3px; bottom: 3px; left: 3px; width: calc((100% - 6px) / 2); border-radius: 999px; background: var(--panel); box-shadow: 0 1px 3px rgba(23, 26, 31, 0.16); content: ""; transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1); } .meet-prompt-modes:has([data-meet-prompt-mode="brief"][aria-pressed="true"])::before { transform: translateX(100%); } .meet-prompt-selector { display: inline-flex; flex: none; align-items: center; gap: 8px; } .meet-prompt-modes-label { color: var(--muted); font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0; text-transform: uppercase; } .meet-prompt-modes button, .tabs button { min-width: 0; min-height: 30px; border: 1px solid transparent; border-radius: 4px; background: transparent; color: var(--muted); } .meet-prompt-modes button { position: relative; z-index: 1; width: 100%; min-height: 28px; padding: 0 8px; border: 0; border-radius: 999px; font-size: var(--fs-sm); font-weight: 600; } .meet-prompt-modes button:hover:not([aria-pressed="true"]) { background: transparent; color: var(--accent-strong); } .meet-prompt-modes button[aria-pressed="true"], .tabs button[aria-pressed="true"] { border-color: var(--line-strong); background: var(--panel); box-shadow: 0 1px 2px rgba(23, 26, 31, 0.08); color: var(--text); font-weight: 700; } .meet-prompt-modes button[aria-pressed="true"] { border-color: transparent; background: transparent; box-shadow: none; color: var(--accent-strong); } .tool-workspace, .kb-workspace, .clipboard-layout { height: 100%; min-height: 0; gap: 1px; padding: 8px; background: var(--line); } .tool-workspace, .kb-workspace { grid-template-columns: minmax(320px, 1fr) minmax(320px, 1fr); } /* Layout tab: the rendered preview is the star — give it the wider column. */ @media (min-width: 981px) { :where(#layoutTool) .tool-workspace { grid-template-columns: minmax(320px, 460px) minmax(0, 1fr); } } .kb-workspace { min-height: 0; } .kb-left { gap: 1px; background: var(--line); } .clipboard-layout { grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); } .panel { border-color: var(--line); border-radius: 0; background: var(--panel); box-shadow: 0 1px 2px rgba(17, 20, 25, 0.04); transition: border-color 160ms ease, box-shadow 160ms ease; } .tool-workspace > .panel:first-child, .kb-workspace > :first-child, .clipboard-layout > :first-child { border-radius: var(--radius) 0 0 var(--radius); } .tool-workspace > .panel:last-child, .kb-workspace > .panel:last-child, .clipboard-layout > :last-child { border-radius: 0 var(--radius) var(--radius) 0; } .panel-title { min-height: 48px; padding: 0 16px; border-bottom: 1px solid var(--line); background: var(--panel); } h2 { font-size: var(--fs-md); font-weight: 700; } .full-body, .layout-output, .panel-body, .raw-body { padding: 0; } textarea { min-width: 0; padding: 18px 20px; resize: none; border: 0; border-radius: 0; background: var(--panel); color: #2d333a; font-size: var(--fs-base); line-height: 1.62; } textarea:focus, .paste-zone:focus, .excel-paste-zone:focus { border-color: transparent; box-shadow: var(--focus-ring-inset); } .paste-zone, .excel-paste-zone { min-height: 100%; border: 0; border-radius: 0; background: var(--panel); } .paste-target-overlay { align-self: center; justify-self: center; width: min(520px, calc(100% - 36px)); padding: 18px; text-align: left; } .paste-target-prompt { margin-bottom: 8px; color: var(--text-strong); font-size: var(--fs-lg); font-style: normal; font-weight: 700; } .paste-target-helper { max-width: 64ch; font-size: var(--fs-base); line-height: 1.55; } .result-meta { min-height: 34px; padding: 8px 14px; border-top: 1px solid var(--line); font-size: var(--fs-sm); } .layout-output { gap: 0; } .layout-preview { border: 0; border-radius: 0; } .editor { padding: 20px 24px 36px; } .preview-empty { justify-content: center; min-height: 100%; padding: 24px; } .prompt-output-shell { min-height: 0; } .prompt-modal-panel, .popover { background: var(--panel); border-color: var(--line); } .popover:not([hidden]), .prompt-modal:not([hidden]) .prompt-modal-panel { animation: floating-surface-enter 160ms ease-out both; } @keyframes tool-view-enter { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } @keyframes floating-surface-enter { from { opacity: 0; transform: translateY(-4px) scale(0.99); } to { opacity: 1; transform: translateY(0) scale(1); } } .mobile-pane-tabs { display: none; } @media (max-width: 900px) { :root { --header-h: 58px; } body { overflow: auto; } .app { grid-template-columns: minmax(0, 1fr); grid-template-rows: var(--header-h) auto minmax(0, 1fr) var(--statusbar-h); height: 100svh; } header { display: none; } .app-topbar { grid-column: 1; grid-row: 1; justify-content: flex-start; gap: 10px; padding: 0 14px; } .mobile-topbar-brand { display: grid; width: 32px; height: 32px; flex: none; place-items: center; border-radius: 5px; background: var(--accent); color: var(--on-accent); font-family: var(--font-mono); font-size: 12px; font-weight: 700; } .active-tool-heading { flex: 1; } .active-tool-heading > span { display: none; } .active-tool-heading h1 { margin: 0; font-size: var(--fs-lg); } .topbar-icon { margin-left: auto; } .mobile-tool-switcher { grid-column: 1; grid-row: 2; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 7px 10px; padding: 8px 14px; border-bottom: 1px solid var(--line); background: var(--panel); } .mobile-tool-switcher > label { align-self: center; color: var(--muted); font-size: var(--fs-xs); font-weight: 700; } .mobile-tool-switcher select { width: 100%; min-width: 0; min-height: 34px; padding: 0 34px 0 10px; border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--panel); color: var(--text); font: inherit; font-weight: 700; } .mobile-release-meta { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 10px; } .mobile-release-meta .contact-link, .mobile-release-meta .version-meta { font-size: 10px; } .tools { grid-column: 1; grid-row: 3; overflow: auto; } .status-bar { grid-column: 1; grid-row: 4; padding: 0 14px; } .toolbar { align-content: flex-start; min-height: 0; padding: 10px 14px; } .toolbar .spacer { display: none; } .meet-prompt-selector { display: grid; grid-template-columns: 1fr; gap: 4px; width: 100%; order: -2; } .meet-prompt-modes { width: 100%; } .meet-prompt-selector + button { order: -1; } .external-group-label { display: none; } .mobile-pane-tabs { display: grid; grid-template-columns: 1fr 1fr; padding: 8px 14px 0; background: var(--bg); } .mobile-pane-tabs button { border: 0; border-bottom: 1px solid var(--line-strong); border-radius: 0; background: transparent; color: var(--muted); } .mobile-pane-tabs button[aria-selected="true"] { border-bottom: 2px solid var(--accent); color: var(--accent-strong); font-weight: 700; } .tool-view.has-mobile-pane-tabs { grid-template-rows: auto auto minmax(0, 1fr); } .tool-workspace, .kb-workspace, .clipboard-layout { display: block; height: auto; min-height: 0; padding: 8px; background: var(--bg); } .tool-view.has-mobile-pane-tabs[data-mobile-pane="input"] .tool-workspace > .panel:last-child, .tool-view.has-mobile-pane-tabs[data-mobile-pane="output"] .tool-workspace > .panel:first-child, .tool-view.has-mobile-pane-tabs[data-mobile-pane="input"] .kb-workspace > .panel:last-child, .tool-view.has-mobile-pane-tabs[data-mobile-pane="output"] .kb-workspace > .kb-left:first-child { display: none; } /* Pane tabs already carry the panel titles — hide the duplicated text, and collapse the title row entirely when it holds nothing else. */ .tool-view.has-mobile-pane-tabs .tool-workspace > .panel > .panel-title > h2, .tool-view.has-mobile-pane-tabs .kb-workspace > .panel > .panel-title > h2 { display: none; } .tool-view.has-mobile-pane-tabs .tool-workspace > .panel > .panel-title:not(:has(> :not(h2))), .tool-view.has-mobile-pane-tabs .kb-workspace > .panel > .panel-title:not(:has(> :not(h2))) { display: none; } .tool-view.has-mobile-pane-tabs .tool-workspace > .panel, .tool-view.has-mobile-pane-tabs .kb-workspace > .panel, .tool-view.has-mobile-pane-tabs .kb-workspace > .kb-left { animation: tool-view-enter 150ms ease-out both; } .tool-workspace > .panel, .kb-workspace > .panel, .clipboard-layout > aside, .clipboard-layout > section { border: 1px solid var(--line); border-radius: var(--radius) !important; } .tool-workspace > .panel + .panel, .kb-workspace > .panel + .panel, .clipboard-layout > section { margin-top: 10px; } .panel-title { min-height: 46px; } .large-textarea, .layout-preview, .prompt-output-shell { min-height: 430px; } .editor { padding: 17px 17px 30px; } .paste-target-overlay { align-self: start; margin-top: 24px; } .raw-grid { grid-template-columns: 1fr; } .popover { max-width: calc(100vw - 24px); } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; } } </style> </head> <body> <svg class="llm-sprite" width="0" height="0" aria-hidden="true" focusable="false" style="position:absolute"> <symbol id="ico-zai" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#3859e8"></rect><path d="M7.5 8.5h9l-9 7h9" fill="none" stroke="#fff" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"></path></symbol> <symbol id="ico-deepseek" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#4d6bfe"></rect><path d="M4.5 12.8c0-2.3 2.2-3.9 5-3.9 3.2 0 5.4 1.8 5.9 4.3.9-.1 1.7-.7 2.1-1.5.3 1.7-.3 3-1.5 3.8.5.2 1.1.2 1.7.1-.9 1.3-2.4 1.7-3.8 1.2-1.6.6-4.2.9-6.3.4-2.9-.7-5.1-2.4-5.1-4.4z" fill="#fff"></path><circle cx="8" cy="12.3" r="1" fill="#4d6bfe"></circle></symbol> <symbol id="ico-gigachat" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#159a57"></rect><path d="M12 5.4c.7 3.4 2.3 4.9 5.6 5.6-3.3.7-4.9 2.2-5.6 5.6-.7-3.4-2.3-4.9-5.6-5.6 3.3-.7 4.9-2.2 5.6-5.6z" fill="#fff"></path></symbol> <symbol id="ico-kimi" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#16182b"></rect><path d="M15 8a5 5 0 1 0 0 8 4 4 0 1 1 0-8z" fill="#fff"></path></symbol> <symbol id="ico-qwen" viewBox="0 0 24 24"><rect width="24" height="24" rx="6" fill="#7a5cf0"></rect><path d="M7.6 8h8.8a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4l-3 2.6V14H7.6a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z" fill="#fff"></path></symbol> </svg> <div class="app"> <header> <div class="brand-row"> <div class="brand" role="img" aria-label="Wiki Lab">WL</div> <div class="brand-copy"> <strong>Wiki Lab</strong> <span>Пространство знаний</span> </div> </div> <nav class="tool-tabs" aria-label="Lab tools"> <div class="tab-group" data-tab-group="generate"> <div class="nav-group-label">Создание</div> <button data-tool-tab="meet-protocol" aria-controls="meetProtocolTool" aria-selected="true" aria-expanded="false"><span>Протокол встречи</span><span class="tab-info" aria-label="Что делает вкладка Протокол встречи?">i</span></button> <button data-tool-tab="knowledge-base" aria-controls="knowledgeBaseTool" aria-selected="false" aria-expanded="false"><span>База знаний</span><span class="tab-info" aria-label="Что делает вкладка База знаний?">i</span></button> <button data-tool-tab="goal-extraction" aria-controls="goalExtractionTool" aria-selected="false" aria-expanded="false"><span>Цель: сбор и обновление</span><span class="tab-info" aria-label="Что делает вкладка Цель: сбор и обновление?">i</span></button> </div> <div class="tab-group" data-tab-group="visualize"> <div class="nav-group-label">Визуализация</div> <button data-tool-tab="layout" aria-controls="layoutTool" aria-selected="false" aria-expanded="false"><span>JSON to SberTrack</span><span class="tab-info" aria-label="Что делает вкладка JSON to SberTrack?">i</span></button> <button data-tool-tab="board" aria-controls="boardTool" aria-selected="false" aria-expanded="false"><span>JSON to Board</span><span class="tab-info" aria-label="Что делает вкладка JSON to Board?">i</span></button> </div> <div class="tab-group" data-tab-group="convert"> <div class="nav-group-label">Конвертация</div> <button data-tool-tab="md-to-wiki" aria-controls="mdToWikiTool" aria-selected="false" aria-expanded="false"><span>Markdown to HTML</span><span class="tab-info" aria-label="Что делает вкладка Markdown to HTML?">i</span></button> <button data-tool-tab="markdown" aria-controls="markdownTool" aria-selected="false" aria-expanded="false"><span>HTML to Markdown</span><span class="tab-info" aria-label="Что делает вкладка HTML to Markdown?">i</span></button> <button data-tool-tab="excel" aria-controls="excelTool" aria-selected="false" aria-expanded="false"><span>WIKI to Excel</span><span class="tab-info" aria-label="Что делает вкладка WIKI to Excel?">i</span></button> <button data-tool-tab="clipboard" aria-controls="clipboardTool" aria-selected="false" aria-expanded="false"><span>Clipboard</span><span class="tab-info" aria-label="Что делает вкладка Clipboard?">i</span></button> </div> </nav> <div class="release-meta"> <a class="contact-link" href="https://sberchat.sberbank.ru/@MMRatnikov" target="_blank" rel="noopener noreferrer">По любым вопросам обращайтесь</a> <span class="version-meta" aria-label="Версия 1.2.13 от 11 августа 2026 года"> <span class="version-number">v1.2.13</span> <time class="version-date" datetime="2026-08-11">11.08.2026</time> </span> </div> </header> <div class="app-topbar"> <div class="mobile-topbar-brand" role="img" aria-label="Wiki Lab">WL</div> <div class="active-tool-heading"> <span id="activeToolGroup">Создание</span> <h1 id="activeToolTitle">Протокол встречи</h1> </div> <button id="activeToolInfo" class="topbar-icon" type="button" data-active-tool-info aria-label="О текущем инструменте" aria-expanded="false">?</button> </div> <div class="mobile-tool-switcher"> <label for="mobileToolSelect">Инструмент</label> <select id="mobileToolSelect" aria-label="Выберите инструмент"> <optgroup label="Создание"> <option value="meet-protocol">Протокол встречи</option> <option value="knowledge-base">База знаний</option> <option value="goal-extraction">Цель: сбор и обновление</option> </optgroup> <optgroup label="Визуализация"> <option value="layout">JSON to SberTrack</option> <option value="board">JSON to Board</option> </optgroup> <optgroup label="Конвертация"> <option value="md-to-wiki">Markdown to HTML</option> <option value="markdown">HTML to Markdown</option> <option value="excel">WIKI to Excel</option> <option value="clipboard">Clipboard</option> </optgroup> </select> <div class="mobile-release-meta"> <a class="contact-link" href="https://sberchat.sberbank.ru/@MMRatnikov" target="_blank" rel="noopener noreferrer">По любым вопросам обращайтесь</a> <span class="version-meta" aria-label="Версия 1.2.13 от 11 августа 2026 года"> <span class="version-number">v1.2.13</span> <time class="version-date" datetime="2026-08-11">11.08.2026</time> </span> </div> </div> <div id="statusBar" class="status-bar" data-state="idle" role="status" aria-live="polite"> <span class="status-prefix">— ОЖИДАНИЕ —</span> <span class="status-message">Ожидание вставки</span> </div> <div id="tabInfoPopover" class="popover" role="group" aria-labelledby="tabInfoPopoverTitle" hidden> <h3 id="tabInfoPopoverTitle" class="popover-title"></h3> <p id="tabInfoPopoverBody" class="popover-body"></p> </div> <main class="tools"> <section id="clipboardTool" class="tool-view" hidden> <div class="toolbar"> <button id="applyRaw">Apply HTML</button> <div class="tabs" aria-label="Editor mode"> <button id="visualTab" aria-pressed="true">Visual</button> <button id="htmlTab" aria-pressed="false">HTML</button> </div> <button id="formatHtmlEditor">Format HTML</button> <button id="syncRaw">Sync From Editor</button> <span class="spacer"></span> <button id="copyPayload">Copy Payload</button> <button id="copyRich" class="btn-primary">Copy Back</button> <button id="clearAll">Очистить</button> </div> <div class="clipboard-layout"> <aside> <div class="panel"> <div class="panel-title"> <h2>Вставка</h2> </div> <div class="panel-body"> <div class="paste-target"> <div id="pasteZone" class="paste-zone" contenteditable="true" role="textbox" aria-label="Paste wiki content here" data-placeholder=""></div> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте фрагмент wiki прямо из браузера — вкладка покажет MIME-типы и payload, даст отредактировать содержимое визуально или как HTML и вернёт результат в wiki через Copy Back.</div> </div> </div> </div> </div> <div class="panel"> <div class="panel-title"> <h2>Форматы</h2> </div> <div class="panel-body"> <div id="typesList" class="types-list"></div> <div class="mini-label">Wiki-элементы</div> <div id="wikiTypesList" class="types-list"></div> <div class="meta-grid"> <div>HTML</div> <code id="htmlSize">0 bytes</code> <div>Текст</div> <code id="textSize">0 bytes</code> <div>Источник</div> <code id="sourceUrl">none</code> </div> </div> </div> </aside> <section class="workspace"> <div class="panel editor-panel"> <div class="panel-title"> <h2>Редактор</h2> </div> <div id="editor" class="editor" contenteditable="true" spellcheck="false" aria-label="Editable wiki content"></div> <textarea id="htmlEditor" class="hidden" spellcheck="false" aria-label="Raw HTML editor"></textarea> </div> <div class="raw-grid"> <div class="panel"> <div class="panel-title"> <h2>Payload</h2> </div> <div class="panel-body raw-body"> <textarea id="payloadDump" readonly spellcheck="false"></textarea> </div> </div> <div class="panel"> <div class="panel-title"> <h2>Исходный HTML</h2> </div> <div class="panel-body raw-body"> <textarea id="originalHtml" readonly spellcheck="false"></textarea> </div> </div> <div class="panel"> <div class="panel-title"> <h2>Обычный текст</h2> </div> <div class="panel-body raw-body"> <textarea id="plainText" spellcheck="false"></textarea> </div> </div> </div> </section> </div> </section> <section id="excelTool" class="tool-view" hidden> <div class="toolbar"> <button id="normalizeExcel">Convert</button> <button id="copyExcelOutput" class="btn-primary">Copy Excel</button> <button id="clearExcelInput" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>Wiki Table HTML</h2> </div> <div class="full-body"> <div class="paste-target"> <div id="excelPasteZone" class="excel-paste-zone" contenteditable="true" role="textbox" aria-label="Paste wiki table here" data-placeholder=""></div> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте wiki-таблицу из браузера — она сразу нормализуется (rowspan/colspan) и будет готова для вставки в Excel, Р7 Офис или Google Sheets.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Excel Output</h2> </div> <div class="full-body"> <div class="prompt-output-shell" data-output-shell="excel"> <textarea id="excelOutput" class="large-textarea" readonly spellcheck="false" aria-label="Excel TSV output"></textarea> <div id="excelOutputEmpty" class="preview-empty prompt-output-empty" data-empty-for="excel-output"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Сконвертируйте таблицу — здесь появится результат для Excel</p> </div> </div> <div id="excelMeta" class="result-meta">Таблица ещё не сконвертирована</div> </div> </section> </div> </section> <section id="layoutTool" class="tool-view" hidden> <div class="toolbar"> <button id="buildLayoutPromptFromText" class="btn-soft">Нет JSON? Собрать промт</button> <button id="renderLayoutJson">Render</button> <button id="copyLayoutRich" class="btn-primary">Copy Back</button> <button id="loadLayoutExample" type="button">Пример</button> <button id="loadKnowledgeBaseExample" type="button">Пример База Знаний</button> <button id="loadProtocolExample" type="button">Пример Протокола</button> <button id="formatLayoutJson">Format JSON</button> <button id="copyLayoutHtml">Save HTML</button> <label class="toolbar-field" for="layoutGoalProfile"> <span>Профиль страницы цели:</span> <select id="layoutGoalProfile"> <option value="public" selected>публичная</option> <option value="audit">аудит</option> </select> </label> <button id="clearLayout" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>Layout JSON</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="layoutJsonInput" class="large-textarea" spellcheck="false" aria-label="Layout JSON input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Принимаются два формата: layout JSON с массивом blocks и JSON цели (goal-template.v1) — формат определяется автоматически, из цели страница собирается сама, профиль переключается в тулбаре. Нет готового JSON? Нажмите «Нет JSON? Собрать промт», вставьте исходный текст — получите промт для LLM. Вставьте JSON сюда: Render запустится автоматически, Copy Back перенесёт результат в wiki. Кнопка «Пример Протокола» подставляет Markdown протокола и сразу рендерит preview.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Wiki Preview</h2> </div> <div class="layout-output"> <div id="layoutPreview" class="editor layout-preview" aria-label="Rendered wiki preview"> <div class="preview-empty" data-empty-for="layout"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Вставьте JSON слева — здесь появится preview</p> </div> </div> <input id="layoutHtmlOutput" type="hidden"> <div id="layoutMeta" class="result-meta">Layout ещё не отрендерен</div> </div> </section> </div> </section> <section id="boardTool" class="tool-view" hidden> <div class="toolbar"> <button id="buildBoardPromptFromText" class="btn-soft">Нет JSON? Собрать промт</button> <button id="parseBoardPayloadBtn">Разобрать</button> <button id="copyBoardPayload" class="btn-primary">Копировать для доски</button> <button id="boardToMarkdown">В Markdown</button> <button id="formatBoardScene">Format JSON</button> <button id="clearBoard" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>Board JSON</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="boardSceneInput" class="large-textarea" spellcheck="false" aria-label="Board scene JSON" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Принимаются три формата, вид определяется сам: фрагмент, скопированный с доски; board-layout-v1 от LLM; разобранная сцена. «Разобрать» приводит поле к board-layout-v1 — этот JSON и отдавайте модели вместе с промтом («Нет JSON? Собрать промт»), чтобы она доработала доску. «Копировать для доски» кладёт нативный payload в буфер одним text/plain. «В Markdown» делает обратное: собирает с доски таблицу объектов — бланки «поле/значение» становятся строками, таблицы одинаковой формы сливаются в одну.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Предупреждения</h2> </div> <div class="full-body"> <div id="boardWarnings" class="result-meta">Ничего не разобрано</div> </div> </section> </div> </section> <section id="meetProtocolTool" class="tool-view"> <div class="toolbar"> <div class="meet-prompt-selector"> <span class="meet-prompt-modes-label">Промт</span> <div class="tabs meet-prompt-modes" role="group" aria-label="Выбор промта встречи"> <button type="button" data-meet-prompt-mode="knowledge-base" aria-pressed="false">Для базы знаний</button> <button type="button" data-meet-prompt-mode="brief" aria-pressed="true">Краткая информация</button> </div> </div> <button id="buildMeetProtocol">Создать промт</button> <button id="prepareMeetProtocol" class="btn-primary">Copy Result</button> <span class="llm-launchers"> <span class="external-group-label">открыть в:</span> <button class="btn-external meet-llm-link" type="button" data-meet-llm-url="https://chat.z.ai/" aria-label="Открыть в Z.ai"><svg class="llm-ico" aria-hidden="true"><use href="#ico-zai"></use></svg><span>Z.ai</span></button> <button class="btn-external meet-llm-link" type="button" data-meet-llm-url="https://chat.deepseek.com/" aria-label="Открыть в DeepSeek"><svg class="llm-ico" aria-hidden="true"><use href="#ico-deepseek"></use></svg><span>DeepSeek</span></button> <button class="btn-external meet-llm-link" type="button" data-meet-llm-url="https://giga.chat/" aria-label="Открыть в GigaChat"><svg class="llm-ico" aria-hidden="true"><use href="#ico-gigachat"></use></svg><span>GigaChat</span></button> <button class="btn-external meet-llm-link" type="button" data-meet-llm-url="https://www.kimi.com/" aria-label="Открыть в Kimi"><svg class="llm-ico" aria-hidden="true"><use href="#ico-kimi"></use></svg><span>Kimi</span></button> <button class="btn-external meet-llm-link" type="button" data-meet-llm-url="https://chat.qwen.ai/" aria-label="Открыть в Qwen"><svg class="llm-ico" aria-hidden="true"><use href="#ico-qwen"></use></svg><span>Qwen</span></button> </span> <button id="clearMeetProtocol" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>Транскрибация</h2> <button id="showMeetProtocolPrompt" class="panel-title-link" type="button">Промт для LLM</button> </div> <div class="full-body"> <div class="paste-target"> <textarea id="meetTranscriptInput" class="large-textarea" spellcheck="false" aria-label="Meeting transcript input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте сырую транскрибацию встречи — вкладка уберёт штампы времени и маркер (распознано), затем соберёт полный промт для LLM. Copy Result копирует его, кнопки внешних LLM копируют и сразу открывают чат.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Результат</h2> </div> <div class="full-body"> <div class="prompt-output-shell" data-output-shell="meet-protocol"> <textarea id="meetProtocolOutput" class="large-textarea" readonly spellcheck="false" aria-label="Meeting protocol prompt output"></textarea> <div id="meetProtocolEmpty" class="preview-empty prompt-output-empty" data-empty-for="meet-protocol-output"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Создайте промт — здесь появится итоговый промт</p> </div> </div> <div id="meetProtocolMeta" class="result-meta">Промт ещё не собран</div> </div> </section> </div> </section> <section id="knowledgeBaseTool" class="tool-view" hidden> <div class="toolbar"> <button id="buildKbPrompt">Создать промт</button> <button id="prepareKbPrompt" class="btn-primary">Copy Result</button> <span class="llm-launchers"> <span class="external-group-label">открыть в:</span> <button class="btn-external kb-llm-link" type="button" data-kb-llm-url="https://chat.z.ai/" aria-label="Открыть в Z.ai"><svg class="llm-ico" aria-hidden="true"><use href="#ico-zai"></use></svg><span>Z.ai</span></button> <button class="btn-external kb-llm-link" type="button" data-kb-llm-url="https://chat.deepseek.com/" aria-label="Открыть в DeepSeek"><svg class="llm-ico" aria-hidden="true"><use href="#ico-deepseek"></use></svg><span>DeepSeek</span></button> <button class="btn-external kb-llm-link" type="button" data-kb-llm-url="https://giga.chat/" aria-label="Открыть в GigaChat"><svg class="llm-ico" aria-hidden="true"><use href="#ico-gigachat"></use></svg><span>GigaChat</span></button> <button class="btn-external kb-llm-link" type="button" data-kb-llm-url="https://www.kimi.com/" aria-label="Открыть в Kimi"><svg class="llm-ico" aria-hidden="true"><use href="#ico-kimi"></use></svg><span>Kimi</span></button> <button class="btn-external kb-llm-link" type="button" data-kb-llm-url="https://chat.qwen.ai/" aria-label="Открыть в Qwen"><svg class="llm-ico" aria-hidden="true"><use href="#ico-qwen"></use></svg><span>Qwen</span></button> </span> <button id="clearKb" class="btn-clear">Очистить</button> </div> <div class="kb-workspace"> <div class="kb-left"> <section class="panel work-panel"> <div class="panel-title"> <h2>Текущая база знаний</h2> <button id="showKbUpdatePrompt" class="panel-title-link" type="button">Промт для LLM</button> </div> <div class="full-body"> <div class="paste-target"> <textarea id="kbCurrentInput" class="large-textarea" spellcheck="false" aria-label="Current knowledge base input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте текущую Markdown-базу знаний (можно прямо из wiki — HTML сконвертируется автоматически). Пустое поле — нормальный кейс первичного заполнения.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Новый протокол встречи</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="kbProtocolInput" class="large-textarea" spellcheck="false" aria-label="New meeting protocol input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте Markdown протокола встречи — можно прямо из wiki, HTML сконвертируется автоматически.</div> </div> </div> </div> </section> </div> <section class="panel work-panel"> <div class="panel-title"> <h2>Итоговый промт</h2> </div> <div class="full-body"> <div class="prompt-output-shell" data-output-shell="knowledge-base"> <textarea id="kbPromptOutput" class="large-textarea" readonly spellcheck="false" aria-label="Knowledge base update prompt output"></textarea> <div id="kbPromptEmpty" class="preview-empty prompt-output-empty" data-empty-for="knowledge-base-output"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Создайте промт — здесь появится итоговый промт для базы знаний</p> </div> </div> <div id="kbMeta" class="result-meta">Промт ещё не собран</div> </div> </section> </div> </section> <section id="goalExtractionTool" class="tool-view" hidden> <div class="toolbar"> <button id="buildGoalExtractionPrompt">Создать промт</button> <button id="prepareGoalExtractionPrompt" class="btn-primary">Copy Result</button> <span class="llm-launchers"> <span class="external-group-label">открыть в:</span> <button class="btn-external goal-extraction-llm-link" type="button" data-goal-extraction-llm-url="https://chat.z.ai/" aria-label="Открыть в Z.ai"><svg class="llm-ico" aria-hidden="true"><use href="#ico-zai"></use></svg><span>Z.ai</span></button> <button class="btn-external goal-extraction-llm-link" type="button" data-goal-extraction-llm-url="https://chat.deepseek.com/" aria-label="Открыть в DeepSeek"><svg class="llm-ico" aria-hidden="true"><use href="#ico-deepseek"></use></svg><span>DeepSeek</span></button> <button class="btn-external goal-extraction-llm-link" type="button" data-goal-extraction-llm-url="https://giga.chat/" aria-label="Открыть в GigaChat"><svg class="llm-ico" aria-hidden="true"><use href="#ico-gigachat"></use></svg><span>GigaChat</span></button> <button class="btn-external goal-extraction-llm-link" type="button" data-goal-extraction-llm-url="https://www.kimi.com/" aria-label="Открыть в Kimi"><svg class="llm-ico" aria-hidden="true"><use href="#ico-kimi"></use></svg><span>Kimi</span></button> <button class="btn-external goal-extraction-llm-link" type="button" data-goal-extraction-llm-url="https://chat.qwen.ai/" aria-label="Открыть в Qwen"><svg class="llm-ico" aria-hidden="true"><use href="#ico-qwen"></use></svg><span>Qwen</span></button> </span> <button id="clearGoalExtraction" class="btn-clear">Очистить</button> </div> <div class="kb-workspace"> <div class="kb-left"> <section class="panel work-panel"> <div class="panel-title"> <h2>Данные по вопросу</h2> <button id="showGoalExtractionPrompt" class="panel-title-link" type="button">Промт для LLM</button> </div> <div class="full-body"> <div class="paste-target"> <textarea id="goalExtractionDataInput" class="large-textarea" spellcheck="false" aria-label="Goal extraction data input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте исходные материалы по одной цели: документы, заметки, письма, протоколы. Единственное обязательное поле вкладки.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Текущий JSON</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="goalExtractionJsonInput" class="large-textarea" spellcheck="false" aria-label="Optional current goal JSON input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Опционально: уже собранный JSON по цели. Если он есть, LLM обновит текущую версию, а не начнёт с нуля.</div> </div> </div> </div> </section> </div> <section class="panel work-panel"> <div class="panel-title"> <h2>Итоговый промт</h2> </div> <div class="full-body"> <div class="prompt-output-shell" data-output-shell="goal-extraction"> <textarea id="goalExtractionPromptOutput" class="large-textarea" readonly spellcheck="false" aria-label="Goal extraction prompt output"></textarea> <div id="goalExtractionPromptEmpty" class="preview-empty prompt-output-empty" data-empty-for="goal-extraction-output"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Создайте промт — здесь появится итоговый промт для извлечения данных по цели</p> </div> </div> <div id="goalExtractionMeta" class="result-meta">Промт ещё не собран</div> </div> </section> </div> <section class="panel work-panel goal-diff-panel"> <div class="panel-title"> <h2>Сверка версий</h2> <span class="panel-title-note">Нейросеть возвращает документ целиком, поэтому потеря куска данных выглядит как обычный ответ. Сверка ловит её механически.</span> </div> <div class="full-body"> <div class="paste-target"> <textarea id="goalDiffNewJson" class="large-textarea" spellcheck="false" aria-label="Updated goal JSON" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте обновлённый JSON, который вернула нейросеть. Он сравнивается с «Текущим JSON» слева: что добавилось, что изменилось и что исчезло.</div> </div> </div> <div class="goal-diff-actions"> <button id="compareGoalVersions" class="btn-soft" type="button">Сравнить с текущим</button> <button id="copyGoalDiffJson" type="button">Копировать обновлённый JSON</button> </div> <div id="goalDiffReport" class="goal-diff-report" aria-live="polite"></div> <div id="goalDiffMeta" class="result-meta">Сверка не запускалась</div> </div> </section> </section> <section id="markdownTool" class="tool-view" hidden> <div class="toolbar"> <button id="convertMarkdown">Convert</button> <button id="copyMarkdown" class="btn-primary">Copy Markdown</button> <button id="clearMarkdown" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>HTML Input</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="markdownHtmlInput" class="large-textarea" spellcheck="false" aria-label="HTML to Markdown input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте фрагмент страницы или text/html — конвертация в Markdown запустится автоматически; результат заберите кнопкой Copy Markdown.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Markdown Output</h2> </div> <div class="full-body"> <div class="prompt-output-shell" data-output-shell="markdown"> <textarea id="markdownOutput" class="large-textarea" readonly spellcheck="false" aria-label="Markdown output"></textarea> <div id="markdownOutputEmpty" class="preview-empty prompt-output-empty" data-empty-for="markdown-output"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Сконвертируйте HTML — здесь появится Markdown</p> </div> </div> <div id="markdownMeta" class="result-meta">HTML ещё не сконвертирован</div> </div> </section> </div> </section> <section id="mdToWikiTool" class="tool-view" hidden> <div class="toolbar"> <button id="convertMdToWiki">Convert</button> <button id="copyMdToWikiRich" class="btn-primary">Copy Back</button> <button id="loadMdToWikiProtocolExample" type="button">Пример Протокола</button> <button id="saveMdToWikiHtml">Save HTML</button> <button id="clearMdToWiki" class="btn-clear">Очистить</button> </div> <div class="tool-workspace"> <section class="panel work-panel"> <div class="panel-title"> <h2>Markdown</h2> </div> <div class="full-body"> <div class="paste-target"> <textarea id="mdToWikiInput" class="large-textarea" spellcheck="false" aria-label="Markdown input" placeholder=" "></textarea> <div class="paste-target-overlay" aria-hidden="true"> <div class="paste-target-prompt">Вставить сюда...</div> <div class="paste-target-helper">Вставьте Markdown-черновик — заголовки, списки и таблицы сразу превратятся в wiki-HTML с preview; Copy Back перенесёт результат в wiki.</div> </div> </div> </div> </section> <section class="panel work-panel"> <div class="panel-title"> <h2>Wiki Preview</h2> </div> <div class="layout-output"> <div id="mdToWikiPreview" class="editor layout-preview" aria-label="Rendered wiki preview"> <div class="preview-empty" data-empty-for="md-to-wiki"> <div class="preview-skeleton" aria-hidden="true"> <div class="sk-heading"></div> <div class="sk-paragraph sk-paragraph-1"></div> <div class="sk-paragraph sk-paragraph-2"></div> <div class="sk-paragraph sk-paragraph-3"></div> <div class="sk-table"> <div></div><div></div><div></div> <div></div><div></div><div></div> <div></div><div></div><div></div> </div> <div class="sk-buttons"> <div></div><div></div> </div> </div> <p class="preview-empty-hint">Вставьте Markdown — здесь появится preview</p> </div> </div> <input id="mdToWikiHtmlOutput" type="hidden"> <div id="mdToWikiMeta" class="result-meta">Markdown ещё не сконвертирован</div> </div> </section> </div> </section> </main> </div> <div id="layoutPromptModal" class="prompt-modal" hidden> <div class="prompt-modal-backdrop" data-modal-close="true"></div> <div class="prompt-modal-panel" role="dialog" aria-labelledby="layoutPromptModalTitle" aria-modal="true"> <div class="prompt-modal-header"> <h2 id="layoutPromptModalTitle">Собрать промт для layout JSON</h2> <button id="closeLayoutPrompt" class="prompt-modal-close" type="button" aria-label="Закрыть">×</button> </div> <div class="toolbar prompt-modal-toolbar"> <button id="copyLayoutPrompt" type="button" disabled>Копировать</button> <span class="external-group-label" id="layoutLlmGroupLabel">открыть в:</span> <span class="llm-open-group" role="group" aria-labelledby="layoutLlmGroupLabel"> <button class="btn-external layout-llm-link" type="button" data-layout-llm-url="https://chat.z.ai/" aria-label="Открыть собранный промт в Z.ai" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-zai"></use></svg><span>Z.ai</span></button> <button class="btn-external layout-llm-link" type="button" data-layout-llm-url="https://chat.deepseek.com/" aria-label="Открыть собранный промт в DeepSeek" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-deepseek"></use></svg><span>DeepSeek</span></button> <button class="btn-external layout-llm-link" type="button" data-layout-llm-url="https://giga.chat/" aria-label="Открыть собранный промт в GigaChat" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-gigachat"></use></svg><span>GigaChat</span></button> <button class="btn-external layout-llm-link" type="button" data-layout-llm-url="https://www.kimi.com/" aria-label="Открыть собранный промт в Kimi" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-kimi"></use></svg><span>Kimi</span></button> <button class="btn-external layout-llm-link" type="button" data-layout-llm-url="https://chat.qwen.ai/" aria-label="Открыть собранный промт в Qwen" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-qwen"></use></svg><span>Qwen</span></button> </span> </div> <p class="prompt-modal-subtitle">Вставьте исходный текст — кнопки соберут промт (правила + ваш текст) и скопируют или откроют его во внешней LLM. Полученный JSON вставьте в поле слева.</p> <label class="prompt-modal-label" for="layoutPromptSource">Исходный текст</label> <textarea id="layoutPromptSource" class="prompt-modal-source" spellcheck="false" placeholder="Вставьте начальный текст, из которого нужно собрать wiki-страницу…"></textarea> </div> </div> <div id="boardPromptModal" class="prompt-modal" hidden> <div class="prompt-modal-backdrop" data-modal-close="true"></div> <div class="prompt-modal-panel" role="dialog" aria-labelledby="boardPromptModalTitle" aria-modal="true"> <div class="prompt-modal-header"> <h2 id="boardPromptModalTitle">Собрать промт для доски</h2> <button id="closeBoardPrompt" class="prompt-modal-close" type="button" aria-label="Закрыть">×</button> </div> <div class="toolbar prompt-modal-toolbar"> <button id="copyBoardPrompt" type="button" disabled>Копировать</button> <span class="external-group-label" id="boardLlmGroupLabel">открыть в:</span> <span class="llm-open-group" role="group" aria-labelledby="boardLlmGroupLabel"> <button class="btn-external board-llm-link" type="button" data-board-llm-url="https://chat.z.ai/" aria-label="Открыть собранный промт в Z.ai" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-zai"></use></svg><span>Z.ai</span></button> <button class="btn-external board-llm-link" type="button" data-board-llm-url="https://chat.deepseek.com/" aria-label="Открыть собранный промт в DeepSeek" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-deepseek"></use></svg><span>DeepSeek</span></button> <button class="btn-external board-llm-link" type="button" data-board-llm-url="https://giga.chat/" aria-label="Открыть собранный промт в GigaChat" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-gigachat"></use></svg><span>GigaChat</span></button> <button class="btn-external board-llm-link" type="button" data-board-llm-url="https://www.kimi.com/" aria-label="Открыть собранный промт в Kimi" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-kimi"></use></svg><span>Kimi</span></button> <button class="btn-external board-llm-link" type="button" data-board-llm-url="https://chat.qwen.ai/" aria-label="Открыть собранный промт в Qwen" disabled><svg class="llm-ico" aria-hidden="true"><use href="#ico-qwen"></use></svg><span>Qwen</span></button> </span> </div> <p class="prompt-modal-subtitle">Вставьте исходный текст — кнопки соберут промт (контракт board-layout-v1 + правила сборки + ваш текст) и скопируют или откроют его во внешней LLM. Полученный JSON вставьте в поле слева. Чтобы обновить существующую доску, добавьте в текст её JSON и опишите правку.</p> <label class="prompt-modal-label" for="boardPromptSource">Исходный текст</label> <textarea id="boardPromptSource" class="prompt-modal-source" spellcheck="false" placeholder="Вставьте текст, из которого нужно собрать доску…"></textarea> </div> </div> <div id="meetProtocolPromptModal" class="prompt-modal" hidden> <div class="prompt-modal-backdrop" data-modal-close="true"></div> <div class="prompt-modal-panel" role="dialog" aria-labelledby="meetProtocolPromptModalTitle" aria-modal="true"> <div class="prompt-modal-header"> <h2 id="meetProtocolPromptModalTitle">Промт для протокола встречи</h2> <button id="closeMeetProtocolPrompt" class="prompt-modal-close" type="button" aria-label="Закрыть">×</button> </div> <p class="prompt-modal-subtitle">Скопируйте базовый промт или подготовьте полный промт с очищенной транскрибацией во вкладке.</p> <pre id="meetProtocolPromptText" class="prompt-modal-body"></pre> <div class="prompt-modal-footer"> <button id="copyMeetProtocolPrompt" class="btn-soft" type="button">Копировать</button> <button id="closeMeetProtocolPromptFooter" type="button">Закрыть</button> </div> </div> </div> <div id="kbUpdatePromptModal" class="prompt-modal" hidden> <div class="prompt-modal-backdrop" data-modal-close="true"></div> <div class="prompt-modal-panel" role="dialog" aria-labelledby="kbUpdatePromptModalTitle" aria-modal="true"> <div class="prompt-modal-header"> <h2 id="kbUpdatePromptModalTitle">Промт для обновления базы знаний</h2> <button id="closeKbUpdatePrompt" class="prompt-modal-close" type="button" aria-label="Закрыть">×</button> </div> <p class="prompt-modal-subtitle">Скопируйте и вставьте в любую LLM, чтобы обновить Markdown-базу знаний по новому протоколу встречи.</p> <pre id="kbUpdatePromptText" class="prompt-modal-body"></pre> <div class="prompt-modal-footer"> <button id="copyKbUpdatePrompt" class="btn-soft" type="button">Копировать</button> <button id="closeKbUpdatePromptFooter" type="button">Закрыть</button> </div> </div> </div> <div id="goalExtractionPromptModal" class="prompt-modal" hidden> <div class="prompt-modal-backdrop" data-modal-close="true"></div> <div class="prompt-modal-panel" role="dialog" aria-labelledby="goalExtractionPromptModalTitle" aria-modal="true"> <div class="prompt-modal-header"> <h2 id="goalExtractionPromptModalTitle">Промт для извлечения цели</h2> <button id="closeGoalExtractionPrompt" class="prompt-modal-close" type="button" aria-label="Закрыть">×</button> </div> <p class="prompt-modal-subtitle">Скопируйте базовый промт или подготовьте полный промт с данными по вопросу во вкладке.</p> <pre id="goalExtractionPromptText" class="prompt-modal-body"></pre> <div class="prompt-modal-footer"> <button id="copyGoalExtractionPrompt" class="btn-soft" type="button">Копировать</button> <button id="closeGoalExtractionPromptFooter" type="button">Закрыть</button> </div> </div> </div> <script> const DEFAULT_TOOL_TAB = "meet-protocol"; const ACTIVE_TAB_STORAGE_KEY = "wcl.activeTab"; const GROUP_TITLES = { generate: "Создание", visualize: "Визуализация", convert: "Конвертация" }; const TAB_DESCRIPTIONS = { "clipboard": { title: "Clipboard", body: "Вставьте содержимое wiki из браузера, отредактируйте его визуально или как HTML и скопируйте обратно. Боковые панели показывают исходный payload, plain-текст и какие типы wiki-элементов обнаружены." }, "excel": { title: "WIKI to Excel", body: "Вставьте wiki-таблицу и нормализуйте её в TSV/HTML, чтобы она корректно вставлялась в Excel, Р7 Офис или Google Sheets без потери структуры. Сохраняются заголовочные строки, объединённые ячейки и порядок столбцов." }, "layout": { title: "JSON to SberTrack", body: "Как сделать layout: возьмите промт по кнопке «Промт для LLM» (контракт wiki-layout и правила сборки JSON-страницы из исходного текста), отдайте его LLM со своим текстом — промт можно дополнять уточняющими инструкциями, чтобы получить нужный вариант данных. Готовый JSON вставьте в textarea — Render запустится автоматически, Copy Back перенесёт wiki-HTML обратно в wiki." }, "board": { title: "JSON to Board", body: "Полный круг работы с доской. Вставьте скопированный с доски фрагмент и нажмите «Разобрать» — получите board-layout-v1: дорожки, таблицы, карточки и связи без пикселей, с осмысленными id. Отдайте этот JSON модели вместе с промтом («Нет JSON? Собрать промт»), попросите доработать, верните ответ в поле и нажмите «Копировать для доски» — в буфер уйдёт нативный payload одним text/plain. Координаты и размеры считает лаб, модель отвечает только за смысл." }, "meet-protocol": { title: "Протокол встречи", body: "Собирает полный промт для LLM в одном из двух режимов: подробный протокол для базы знаний или краткий связный рассказ. Очищает транскрибацию от ведущих штампов времени и маркера «(распознано)» после ФИО, затем добавляет текст встречи к выбранному промту. Кнопки Z.ai, DeepSeek, GigaChat и Kimi копируют готовый промт и сразу открывают выбранный чат." }, "knowledge-base": { title: "База знаний", body: "Собирает промт для обновления Markdown-базы знаний по новому протоколу встречи. Слева — два поля (текущая база и новый протокол); каждое принимает чистый Markdown или wiki HTML — HTML конвертируется в Markdown на лету при вставке. Справа — итоговый промт, готовый к копированию в LLM (Z.ai, DeepSeek, GigaChat, Kimi)." }, "goal-extraction": { title: "Цель: сбор и обновление", body: "Помогает превратить исходные материалы по одной цели в полную структурированную картину: собирает факты о том, что это за цель, зачем она нужна, где применяется, какие есть сроки, метрики, требования, ответственные, действия, риски и открытые вопросы. Если уже есть собранный JSON, вкладка добавит его как текущую версию — нейросеть обновит данные, а не начнёт с нуля. Обновлённый ответ вставьте в «Сверку версий»: она покажет, что добавилось, что изменилось и что исчезло, чтобы обновление не потеряло уже собранное. Готовый JSON вставляется во вкладку «Layout JSON to Wiki» — она сама соберёт из него страницу." }, "markdown": { title: "HTML to Markdown", body: "Конвертирует wiki-HTML в Markdown для использования вне wiki — issues, документация, черновики. Таблицы становятся Markdown-таблицами; ссылки и inline-форматирование сохраняются." }, "md-to-wiki": { title: "Markdown to HTML", body: "Конвертирует Markdown обратно в wiki-HTML, чтобы можно было вставить Markdown-черновики в wiki без потери форматирования. Заголовки, списки, таблицы и ссылки переносятся чисто." } }; const state = { originalHtml: "", originalPlain: "", currentHtml: "", currentPlain: "", types: [], sourceUrl: "", layoutHtml: "", mdToWikiHtml: "", excelSourceHtml: "", meetPromptMode: "brief", lastEditedSurface: null }; const el = { status: document.getElementById("statusBar"), applyRaw: document.getElementById("applyRaw"), syncRaw: document.getElementById("syncRaw"), copyRich: document.getElementById("copyRich"), clearAll: document.getElementById("clearAll"), pasteZone: document.getElementById("pasteZone"), typesList: document.getElementById("typesList"), wikiTypesList: document.getElementById("wikiTypesList"), htmlSize: document.getElementById("htmlSize"), textSize: document.getElementById("textSize"), sourceUrl: document.getElementById("sourceUrl"), editor: document.getElementById("editor"), htmlEditor: document.getElementById("htmlEditor"), payloadDump: document.getElementById("payloadDump"), originalHtml: document.getElementById("originalHtml"), plainText: document.getElementById("plainText"), copyPayload: document.getElementById("copyPayload"), visualTab: document.getElementById("visualTab"), htmlTab: document.getElementById("htmlTab"), formatHtmlEditor: document.getElementById("formatHtmlEditor"), activeToolGroup: document.getElementById("activeToolGroup"), activeToolTitle: document.getElementById("activeToolTitle"), activeToolInfo: document.getElementById("activeToolInfo"), mobileToolSelect: document.getElementById("mobileToolSelect"), toolTabs: Array.from(document.querySelectorAll("[data-tool-tab]")), toolViews: { clipboard: document.getElementById("clipboardTool"), excel: document.getElementById("excelTool"), layout: document.getElementById("layoutTool"), board: document.getElementById("boardTool"), "meet-protocol": document.getElementById("meetProtocolTool"), "knowledge-base": document.getElementById("knowledgeBaseTool"), "goal-extraction": document.getElementById("goalExtractionTool"), markdown: document.getElementById("markdownTool"), "md-to-wiki": document.getElementById("mdToWikiTool") }, excelPasteZone: document.getElementById("excelPasteZone"), excelOutput: document.getElementById("excelOutput"), excelOutputEmpty: document.getElementById("excelOutputEmpty"), excelMeta: document.getElementById("excelMeta"), clearExcelInput: document.getElementById("clearExcelInput"), normalizeExcel: document.getElementById("normalizeExcel"), copyExcelOutput: document.getElementById("copyExcelOutput"), layoutJsonInput: document.getElementById("layoutJsonInput"), boardSceneInput: document.getElementById("boardSceneInput"), boardPromptModal: document.getElementById("boardPromptModal"), boardPromptSource: document.getElementById("boardPromptSource"), buildBoardPromptFromText: document.getElementById("buildBoardPromptFromText"), copyBoardPrompt: document.getElementById("copyBoardPrompt"), closeBoardPrompt: document.getElementById("closeBoardPrompt"), boardLlmLinks: Array.from(document.querySelectorAll("[data-board-llm-url]")), boardWarnings: document.getElementById("boardWarnings"), parseBoardPayloadBtn: document.getElementById("parseBoardPayloadBtn"), copyBoardPayload: document.getElementById("copyBoardPayload"), formatBoardScene: document.getElementById("formatBoardScene"), boardToMarkdown: document.getElementById("boardToMarkdown"), clearBoard: document.getElementById("clearBoard"), layoutHtmlOutput: document.getElementById("layoutHtmlOutput"), layoutPreview: document.getElementById("layoutPreview"), layoutMeta: document.getElementById("layoutMeta"), clearLayout: document.getElementById("clearLayout"), formatLayoutJson: document.getElementById("formatLayoutJson"), renderLayoutJson: document.getElementById("renderLayoutJson"), layoutGoalProfile: document.getElementById("layoutGoalProfile"), loadLayoutExample: document.getElementById("loadLayoutExample"), loadKnowledgeBaseExample: document.getElementById("loadKnowledgeBaseExample"), loadProtocolExample: document.getElementById("loadProtocolExample"), copyLayoutHtml: document.getElementById("copyLayoutHtml"), copyLayoutRich: document.getElementById("copyLayoutRich"), meetTranscriptInput: document.getElementById("meetTranscriptInput"), meetProtocolOutput: document.getElementById("meetProtocolOutput"), meetProtocolEmpty: document.getElementById("meetProtocolEmpty"), meetProtocolMeta: document.getElementById("meetProtocolMeta"), meetPromptModeButtons: Array.from(document.querySelectorAll("[data-meet-prompt-mode]")), buildMeetProtocol: document.getElementById("buildMeetProtocol"), prepareMeetProtocol: document.getElementById("prepareMeetProtocol"), clearMeetProtocol: document.getElementById("clearMeetProtocol"), kbCurrentInput: document.getElementById("kbCurrentInput"), kbProtocolInput: document.getElementById("kbProtocolInput"), kbPromptOutput: document.getElementById("kbPromptOutput"), kbPromptEmpty: document.getElementById("kbPromptEmpty"), kbMeta: document.getElementById("kbMeta"), showKbUpdatePrompt: document.getElementById("showKbUpdatePrompt"), buildKbPrompt: document.getElementById("buildKbPrompt"), prepareKbPrompt: document.getElementById("prepareKbPrompt"), clearKb: document.getElementById("clearKb"), goalExtractionDataInput: document.getElementById("goalExtractionDataInput"), goalExtractionJsonInput: document.getElementById("goalExtractionJsonInput"), goalExtractionPromptOutput: document.getElementById("goalExtractionPromptOutput"), goalExtractionPromptEmpty: document.getElementById("goalExtractionPromptEmpty"), goalExtractionMeta: document.getElementById("goalExtractionMeta"), goalDiffNewJson: document.getElementById("goalDiffNewJson"), compareGoalVersions: document.getElementById("compareGoalVersions"), copyGoalDiffJson: document.getElementById("copyGoalDiffJson"), goalDiffReport: document.getElementById("goalDiffReport"), goalDiffMeta: document.getElementById("goalDiffMeta"), showGoalExtractionPrompt: document.getElementById("showGoalExtractionPrompt"), buildGoalExtractionPrompt: document.getElementById("buildGoalExtractionPrompt"), prepareGoalExtractionPrompt: document.getElementById("prepareGoalExtractionPrompt"), clearGoalExtraction: document.getElementById("clearGoalExtraction"), markdownHtmlInput: document.getElementById("markdownHtmlInput"), markdownOutput: document.getElementById("markdownOutput"), markdownOutputEmpty: document.getElementById("markdownOutputEmpty"), markdownMeta: document.getElementById("markdownMeta"), clearMarkdown: document.getElementById("clearMarkdown"), convertMarkdown: document.getElementById("convertMarkdown"), copyMarkdown: document.getElementById("copyMarkdown"), mdToWikiInput: document.getElementById("mdToWikiInput"), mdToWikiPreview: document.getElementById("mdToWikiPreview"), mdToWikiHtmlOutput: document.getElementById("mdToWikiHtmlOutput"), mdToWikiMeta: document.getElementById("mdToWikiMeta"), clearMdToWiki: document.getElementById("clearMdToWiki"), convertMdToWiki: document.getElementById("convertMdToWiki"), loadMdToWikiProtocolExample: document.getElementById("loadMdToWikiProtocolExample"), saveMdToWikiHtml: document.getElementById("saveMdToWikiHtml"), copyMdToWikiRich: document.getElementById("copyMdToWikiRich"), tabInfoButtons: Array.from(document.querySelectorAll("[data-tool-tab], [data-active-tool-info]")), tabInfoPopover: document.getElementById("tabInfoPopover"), tabInfoPopoverTitle: document.getElementById("tabInfoPopoverTitle"), tabInfoPopoverBody: document.getElementById("tabInfoPopoverBody"), buildLayoutPromptFromText: document.getElementById("buildLayoutPromptFromText"), layoutPromptModal: document.getElementById("layoutPromptModal"), layoutPromptSource: document.getElementById("layoutPromptSource"), layoutLlmLinks: Array.from(document.querySelectorAll(".layout-llm-link")), copyLayoutPrompt: document.getElementById("copyLayoutPrompt"), closeLayoutPrompt: document.getElementById("closeLayoutPrompt"), showMeetProtocolPrompt: document.getElementById("showMeetProtocolPrompt"), meetProtocolPromptModal: document.getElementById("meetProtocolPromptModal"), meetProtocolPromptText: document.getElementById("meetProtocolPromptText"), copyMeetProtocolPrompt: document.getElementById("copyMeetProtocolPrompt"), closeMeetProtocolPrompt: document.getElementById("closeMeetProtocolPrompt"), closeMeetProtocolPromptFooter: document.getElementById("closeMeetProtocolPromptFooter"), kbUpdatePromptModal: document.getElementById("kbUpdatePromptModal"), kbUpdatePromptText: document.getElementById("kbUpdatePromptText"), closeKbUpdatePrompt: document.getElementById("closeKbUpdatePrompt"), closeKbUpdatePromptFooter: document.getElementById("closeKbUpdatePromptFooter"), copyKbUpdatePrompt: document.getElementById("copyKbUpdatePrompt"), goalExtractionPromptModal: document.getElementById("goalExtractionPromptModal"), goalExtractionPromptText: document.getElementById("goalExtractionPromptText"), copyGoalExtractionPrompt: document.getElementById("copyGoalExtractionPrompt"), closeGoalExtractionPrompt: document.getElementById("closeGoalExtractionPrompt"), closeGoalExtractionPromptFooter: document.getElementById("closeGoalExtractionPromptFooter"), closeGoalReviewPromptFooter: document.getElementById("closeGoalReviewPromptFooter") }; function byteSize(value) { return `${new Blob([value || ""]).size} bytes`; } const FLASH_PREV_LABEL = new WeakMap(); function flashCopySuccess(btn, opts = {}) { if (!btn || FLASH_PREV_LABEL.has(btn)) return; FLASH_PREV_LABEL.set(btn, btn.textContent); btn.classList.add("btn-flash-ok"); btn.disabled = true; btn.textContent = opts.label || "Скопировано ✓"; const duration = typeof opts.duration === "number" ? opts.duration : 900; setTimeout(() => { const original = FLASH_PREV_LABEL.get(btn); if (original !== undefined) btn.textContent = original; btn.classList.remove("btn-flash-ok"); btn.disabled = false; FLASH_PREV_LABEL.delete(btn); }, duration); } function setPreviewEmpty(container, isEmpty) { if (!container) return; const emptyEl = container.querySelector(".preview-empty"); if (emptyEl) emptyEl.hidden = !isEmpty; } function setPromptOutputEmpty(output, emptyEl) { if (!output || !emptyEl) return; const hasValue = Boolean(output.value.trim()); emptyEl.hidden = hasValue; output.disabled = !hasValue; } function renderIntoPreview(container, html) { if (!container) return; const emptyEl = container.querySelector(".preview-empty"); container.innerHTML = ""; if (html) container.insertAdjacentHTML("beforeend", html); if (emptyEl) { emptyEl.hidden = Boolean(html); container.appendChild(emptyEl); } else { container.innerHTML = html; } } const STATUS_PREFIX_BY_STATE = { idle: "— ОЖИДАНИЕ —", ok: "— ГОТОВО —", warn: "— ВНИМАНИЕ —", bad: "— ОШИБКА —", "": "— ОЖИДАНИЕ —" }; function setStatus(message, kind = "") { const state = kind || "idle"; el.status.dataset.state = state; const prefixEl = el.status.querySelector(".status-prefix"); const messageEl = el.status.querySelector(".status-message"); if (prefixEl) prefixEl.textContent = STATUS_PREFIX_BY_STATE[state] || "— ОЖИДАНИЕ —"; if (messageEl) messageEl.textContent = message; } function activateToolTab(name, { persist = true } = {}) { for (const tab of el.toolTabs) { const active = tab.dataset.toolTab === name; tab.setAttribute("aria-selected", String(active)); } for (const [key, view] of Object.entries(el.toolViews)) { view.hidden = key !== name; } const selectedTab = el.toolTabs.find((tab) => tab.dataset.toolTab === name); const group = selectedTab?.closest("[data-tab-group]")?.dataset.tabGroup; const description = TAB_DESCRIPTIONS[name]; el.activeToolGroup.textContent = GROUP_TITLES[group] ?? "Создание"; el.activeToolTitle.textContent = description?.title || selectedTab?.innerText.trim() || name; el.activeToolInfo.dataset.toolTab = name; el.activeToolInfo.setAttribute("aria-label", `О разделе «${el.activeToolTitle.textContent}»`); el.mobileToolSelect.value = name; hideTabInfoPopover(); if (persist) { try { localStorage.setItem(ACTIVE_TAB_STORAGE_KEY, name); } catch { // private mode / disabled storage — silently skip } } } function initMobilePaneTabs() { document.querySelectorAll(".tool-workspace, .kb-workspace").forEach((workspace) => { const panels = Array.from(workspace.children); if (panels.length !== 2) return; const view = workspace.closest(".tool-view"); if (!view || view.querySelector(":scope > .mobile-pane-tabs")) return; const firstTitle = panels[0].querySelector("h2")?.textContent.trim() || "Вход"; const secondTitle = panels[1].querySelector("h2")?.textContent.trim() || "Результат"; const tabs = document.createElement("div"); tabs.className = "mobile-pane-tabs"; tabs.setAttribute("role", "tablist"); tabs.setAttribute("aria-label", "Область инструмента"); for (const [pane, title] of [["input", firstTitle], ["output", secondTitle]]) { const button = document.createElement("button"); button.type = "button"; button.textContent = title; button.dataset.mobilePaneTarget = pane; button.setAttribute("role", "tab"); button.setAttribute("aria-selected", String(pane === "input")); button.addEventListener("click", () => { view.dataset.mobilePane = pane; for (const other of tabs.querySelectorAll("button")) { other.setAttribute("aria-selected", String(other === button)); } }); tabs.appendChild(button); } view.classList.add("has-mobile-pane-tabs"); view.dataset.mobilePane = "input"; view.insertBefore(tabs, workspace); }); } function scheduleAfterPaste(action) { window.setTimeout(() => action(), 0); } function setMode(mode) { const htmlMode = mode === "html"; el.editor.classList.toggle("hidden", htmlMode); el.htmlEditor.classList.toggle("hidden", !htmlMode); el.visualTab.setAttribute("aria-pressed", String(!htmlMode)); el.htmlTab.setAttribute("aria-pressed", String(htmlMode)); if (htmlMode) { if (state.lastEditedSurface === "html") return; syncFromEditor(); } else { if (state.lastEditedSurface === "visual") return; applyHtmlToEditor(el.htmlEditor.value); } } function renderTypes() { el.typesList.replaceChildren(); if (!state.types.length) { const item = document.createElement("span"); item.className = "type-pill"; item.textContent = "none"; el.typesList.append(item); return; } for (const type of state.types) { const item = document.createElement("span"); item.className = "type-pill"; item.textContent = type; el.typesList.append(item); } } function collectWikiTypes(html) { const template = document.createElement("template"); template.innerHTML = html || ""; const counts = new Map(); for (const node of template.content.querySelectorAll("[data-type]")) { const type = node.getAttribute("data-type"); counts.set(type, (counts.get(type) || 0) + 1); } return Array.from(counts.entries()) .sort(([left], [right]) => left.localeCompare(right)) .map(([type, count]) => ({ type, count })); } function renderWikiTypes() { el.wikiTypesList.replaceChildren(); const wikiTypes = collectWikiTypes(getCurrentHtml()); if (!wikiTypes.length) { const item = document.createElement("span"); item.className = "type-pill"; item.textContent = "none"; el.wikiTypesList.append(item); return; } for (const { type, count } of wikiTypes) { const item = document.createElement("span"); item.className = "type-pill wiki"; item.textContent = `${type} × ${count}`; el.wikiTypesList.append(item); } } function updateMeta() { const html = getCurrentHtml(); const text = getCurrentPlainText(); el.htmlSize.textContent = byteSize(html); el.textSize.textContent = byteSize(text); el.sourceUrl.textContent = state.sourceUrl || "none"; renderTypes(); renderWikiTypes(); } function buildPayloadDump() { const copyBackHtml = state.currentHtml || getCurrentHtml(); const copyBackPlain = state.currentPlain || getCurrentPlainText(); return JSON.stringify({ mimeTypes: state.types, sourceUrl: state.sourceUrl || null, original: { "text/html": state.originalHtml, "text/html:formatted": formatHtml(state.originalHtml), "text/plain": state.originalPlain }, copyBack: { "text/html": copyBackHtml, "text/html:formatted": formatHtml(copyBackHtml), "text/plain": copyBackPlain } }, null, 2); } function refreshPayloadDump() { el.payloadDump.value = buildPayloadDump(); } function getCurrentHtml() { if (!el.htmlEditor.classList.contains("hidden")) { return el.htmlEditor.value; } return el.editor.innerHTML; } function getCurrentPlainText() { return el.plainText.value; } // __INJECT_LLM_PROMPT__ // __INJECT_BOARD_PROMPT__ // __INJECT_MEET_PROTOCOL_PROMPT__ // __INJECT_MEETING_NARRATIVE_PROMPT__ // __INJECT_KB_UPDATE_PROMPT__ // __INJECT_GOAL_EXTRACTION_PROMPT__ // __INJECT_KNOWLEDGE_BASE_LAYOUT__ // __INJECT_PROTOCOL_EXAMPLE_MARKDOWN__ // __INJECT_CONVERTERS__ const MEET_PROMPT_CONFIGS = Object.freeze({ "knowledge-base": Object.freeze({ label: "Для базы знаний", text: MEET_PROTOCOL_PROMPT_TEXT }), brief: Object.freeze({ label: "Краткая информация", text: MEETING_NARRATIVE_PROMPT_TEXT }) }); function getMeetPromptConfig() { return MEET_PROMPT_CONFIGS[state.meetPromptMode] || MEET_PROMPT_CONFIGS.brief; } function updateMeetPromptModeUi() { const config = getMeetPromptConfig(); el.meetPromptModeButtons.forEach((button) => { button.setAttribute("aria-pressed", String(button.dataset.meetPromptMode === state.meetPromptMode)); }); document.getElementById("meetProtocolPromptModalTitle").textContent = `Промт: ${config.label}`; if (!el.meetProtocolPromptModal.hidden) { el.meetProtocolPromptText.textContent = config.text; } } function selectMeetPromptMode(mode) { if (!Object.prototype.hasOwnProperty.call(MEET_PROMPT_CONFIGS, mode)) return; state.meetPromptMode = mode; updateMeetPromptModeUi(); if (el.meetTranscriptInput.value.trim()) { prepareMeetProtocolPrompt(); return; } el.meetProtocolOutput.value = ""; el.meetProtocolMeta.textContent = `Выбран промт: ${getMeetPromptConfig().label}`; setPromptOutputEmpty(el.meetProtocolOutput, el.meetProtocolEmpty); setStatus(`Выбран промт: ${getMeetPromptConfig().label}`, "ok"); } function getPrimaryTable() { const source = document.createElement("template"); source.innerHTML = getCurrentHtml(); return source.content.querySelector("table"); } function tablesFromHtml(value) { const source = document.createElement("template"); source.innerHTML = value || ""; return topLevelTables(source.content); } // Протокол из Markdown приносит десяток таблиц сразу, поэтому счётчики // говорят и про их число: молчаливая конвертация одной первой выглядела // как успех, а девять таблиц при этом терялись. function pluralRu(count, one, few, many) { const mod10 = count % 10; const mod100 = count % 100; if (mod10 === 1 && mod100 !== 11) return one; if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return few; return many; } function excelSummary(payload) { if (payload.tables > 1) { const tables = `${payload.tables} ${pluralRu(payload.tables, "таблица", "таблицы", "таблиц")}`; return `${tables} подряд, строк: ${payload.contentRows}`; } const dataRows = payload.contentRows - 1; return `${dataRows} ${pluralRu(dataRows, "строка", "строки", "строк")} + заголовок`; } function excelCountsLabel(payload) { return `${excelSummary(payload)}, колонок: ${payload.width}`; } function normalizeExcelInput() { const tables = tablesFromHtml(state.excelSourceHtml || el.excelPasteZone.innerHTML); if (!tables.length) { el.excelOutput.value = ""; el.excelMeta.textContent = "Таблица не найдена"; setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); setStatus("Таблица для конвертации в Excel не найдена", "warn"); return; } const payload = normalizeTablesToExcelPayload(tables); if (!payload.rows.length) { el.excelOutput.value = ""; el.excelMeta.textContent = "Таблица пустая"; setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); setStatus("Таблица пустая", "warn"); return; } el.excelOutput.value = excelTsvFromRows(payload.rows); el.excelMeta.textContent = excelCountsLabel(payload); setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); setStatus(`Готово для Excel: ${excelSummary(payload)}`, "ok"); } function loadExcelClipboardPayload(clipboardData) { const html = clipboardData?.getData("text/html") || ""; const plain = clipboardData?.getData("text/plain") || ""; const source = html || `<table><tbody>${plain.split(/\r?\n/).map((line) => { return `<tr>${line.split("\t").map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`; }).join("")}</tbody></table>`; state.excelSourceHtml = formatHtml(source); el.excelPasteZone.innerHTML = html || `<pre>${escapeHtml(plain)}</pre>`; normalizeExcelInput(); } function writeClipboardViaCopyEvent(html, text) { // Sync copy-event path: the browser stores the payload verbatim (no async // clipboard sanitizer), which strict parsers like Р7/OnlyOffice need. // Pass text === null to publish text/html alone: the wiki editor prefers // text/plain when both are offered and turns its tabs into one big table. // Pass html === null for the mirror case — доска читает только text/plain. const scratch = document.createElement("textarea"); scratch.value = " "; scratch.setAttribute("readonly", ""); scratch.style.position = "fixed"; scratch.style.left = "-9999px"; document.body.appendChild(scratch); scratch.select(); let handled = false; const listener = (copyEvent) => { copyEvent.preventDefault(); if (html !== null) { copyEvent.clipboardData.setData("text/html", html); } if (text !== null) { copyEvent.clipboardData.setData("text/plain", text); } handled = true; }; let executed = false; document.addEventListener("copy", listener, true); try { executed = document.execCommand("copy"); } finally { document.removeEventListener("copy", listener, true); document.body.removeChild(scratch); } return handled && executed; } async function copyExcelOutput(event) { const tables = tablesFromHtml(state.excelSourceHtml || el.excelPasteZone.innerHTML); if (!tables.length) { setStatus("Таблица для копирования не найдена", "warn"); return; } const payload = normalizeTablesToExcelPayload(tables); const { rows, html } = payload; if (!rows.length) { setStatus("Таблица пустая", "warn"); return; } const tsv = excelTsvFromRows(rows); el.excelOutput.value = tsv; setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); try { if (!writeClipboardViaCopyEvent(html, tsv)) { await navigator.clipboard.write([ new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }), "text/plain": new Blob([tsv], { type: "text/plain" }) }) ]); } setStatus(`Скопировано для Excel: ${excelSummary(payload)}`, "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const text = el.excelOutput.value; el.excelOutput.focus(); el.excelOutput.select(); const ok = document.execCommand("copy"); setStatus(ok ? "TSV скопирован через выделение" : `Не удалось скопировать TSV: ${error.message}`, ok ? "ok" : "bad"); if (ok) flashCopySuccess(event && event.currentTarget); } } // Вкладка принимает два формата: layout JSON (wiki-layout-v1) и JSON цели // (goal-template.v1). Второй распознаётся по schema_version, а при его отсутствии — // по обязательным для цели полям, и превращается в layout детерминированным маппером. function isGoalTemplateJson(parsed) { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false; if (parsed.schema_version === "goal-template.v1") return true; if (typeof parsed.$schema === "string" && parsed.$schema.includes("goal-template.v1")) return true; return Boolean(parsed.goal_identity && parsed.source_refs && !Array.isArray(parsed.blocks)); } function renderLayoutJson() { let parsed; try { parsed = JSON.parse(el.layoutJsonInput.value); } catch (error) { setPreviewEmpty(el.layoutPreview, true); el.layoutMeta.textContent = `Ошибка JSON: ${error.message}`; setStatus(`Ошибка в layout JSON: ${error.message}`, "bad"); return; } const fromGoal = isGoalTemplateJson(parsed); const profile = el.layoutGoalProfile?.value === "audit" ? "audit" : "public"; let layout = parsed; let shapeProblems = []; if (fromGoal) { // Схема требует строки в title/name, но модель нередко присылает textField. // Страница всё равно собирается, а пользователь узнаёт, что JSON не по схеме. shapeProblems = goalShapeProblems(parsed); try { layout = goalToLayout(parsed, { profile }); } catch (error) { setPreviewEmpty(el.layoutPreview, true); el.layoutMeta.textContent = `Ошибка сборки страницы цели: ${error.message}`; setStatus(`Не удалось собрать страницу из JSON цели: ${error.message}`, "bad"); return; } } if (!Array.isArray(layout.blocks)) { setPreviewEmpty(el.layoutPreview, true); el.layoutMeta.textContent = 'Ожидается top-level массив "blocks"'; setStatus('В layout JSON нужен top-level массив "blocks"', "bad"); return; } try { const blocksHtml = renderLayoutBlocks(layout.blocks); const html = `${blocksHtml}\n`; const types = Array.from(new Set(Array.from(html.matchAll(/data-type="([^"]+)"/g)).map((match) => match[1]))).sort(); const gridCounts = Array.from(html.matchAll(/blockscount="(\d+)"/g)).map((match) => Number.parseInt(match[1], 10)); // Rich "Copy Back" pastes state.layoutHtml as text/html; keep it compact so the // wiki editor doesn't turn formatHtml's inter-tag newlines into stray <br>. state.layoutHtml = blocksHtml; el.layoutHtmlOutput.value = formatHtml(html); renderIntoPreview(el.layoutPreview, html); enhanceTabsInPreview(el.layoutPreview); const source = fromGoal ? `JSON цели → страница (профиль: ${profile === "audit" ? "аудит" : "публичная"}), ` : ""; const shapeNote = shapeProblems.length ? ` | JSON не по схеме: ${shapeProblems.join("; ")}` : ""; el.layoutMeta.textContent = `${source}${layout.blocks.length} top-level blocks, data-types: ${types.join(", ") || "none"}, blocksGrid: ${gridCounts.join(", ") || "none"}${shapeNote}`; if (shapeProblems.length) { setStatus(`Страница собрана, но JSON не по схеме: ${shapeProblems[0]}`, "warn"); } else { setStatus(fromGoal ? "Страница цели собрана и отрендерена" : "Layout JSON отрендерен", "ok"); } } catch (error) { setPreviewEmpty(el.layoutPreview, true); el.layoutMeta.textContent = `Ошибка рендера: ${error.message}`; setStatus(`Ошибка рендера layout: ${error.message}`, "bad"); } } function enhanceTabsInPreview(root) { const wrappers = root.querySelectorAll('.tabsWrapper_d6kqf'); wrappers.forEach((wrapper) => { if (wrapper.dataset.previewEnhanced === "1") return; const tabs = Array.from(wrapper.children).filter((child) => child.classList.contains("tabWrapper_z4u1j")); if (tabs.length === 0) return; wrapper.dataset.previewEnhanced = "1"; const bar = document.createElement("div"); bar.className = "preview-tab-bar"; const activate = (index) => { tabs.forEach((tab, i) => { tab.setAttribute("data-preview-active", i === index ? "true" : "false"); }); Array.from(bar.children).forEach((button, i) => { button.setAttribute("aria-selected", i === index ? "true" : "false"); }); }; tabs.forEach((tab, index) => { const label = tab.getAttribute("label") || tab.querySelector(".tabTitle_z4u1j")?.textContent || `Вкладка ${index + 1}`; const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.setAttribute("role", "tab"); button.setAttribute("aria-selected", index === 0 ? "true" : "false"); button.addEventListener("click", () => activate(index)); bar.appendChild(button); }); wrapper.insertBefore(bar, tabs[0]); activate(0); }); } function loadLayoutExample() { const match = LLM_PROMPT_TEXT.match(/```json renderable-smoke-layout\s*\n([\s\S]*?)\n```/); if (!match) { setStatus("В промте не найден блок renderable-smoke-layout", "bad"); return; } el.layoutJsonInput.value = match[1]; renderLayoutJson(); } function loadKnowledgeBaseExample() { el.layoutJsonInput.value = KNOWLEDGE_BASE_LAYOUT_TEXT; renderLayoutJson(); } function loadProtocolExample() { const markdown = PROTOCOL_EXAMPLE_MARKDOWN_TEXT; const html = markdownToWikiHtml(markdown); el.layoutJsonInput.value = ""; state.layoutHtml = html; el.layoutHtmlOutput.value = formatHtml(html); renderIntoPreview(el.layoutPreview, html); el.layoutMeta.textContent = `Строк Markdown в примере протокола: ${markdown.split(/\r?\n/).filter((line) => line.trim()).length}`; setStatus("Пример протокола (Markdown) отрендерен", "ok"); } function formatLayoutJson() { try { el.layoutJsonInput.value = JSON.stringify(JSON.parse(el.layoutJsonInput.value), null, 2); setStatus("Layout JSON отформатирован", "ok"); } catch (error) { setStatus(`Ошибка в layout JSON: ${error.message}`, "bad"); } } function copyTextWithFallback(text, successMessage, failurePrefix) { if (!text.trim()) { setStatus("Нечего копировать", "warn"); return false; } const scratch = document.createElement("textarea"); scratch.value = text; scratch.setAttribute("readonly", ""); scratch.style.position = "fixed"; scratch.style.left = "-9999px"; document.body.append(scratch); scratch.select(); const ok = document.execCommand("copy"); scratch.remove(); setStatus(ok ? successMessage : failurePrefix, ok ? "ok" : "bad"); return ok; } async function copyLayoutRich(event) { const html = state.layoutHtml || el.layoutHtmlOutput.value || el.layoutPreview.innerHTML; if (!html.trim()) { setStatus("Нет wiki-контента для копирования", "warn"); return; } try { // text/html only, written verbatim through the copy event. Offered both // flavours, the wiki editor takes text/plain and rebuilds the page as a // single tab-separated table; without text/plain it has to read the HTML. const verbatim = writeClipboardViaCopyEvent(html, null); if (!verbatim) { await navigator.clipboard.write([ new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }) }) ]); } setStatus(verbatim ? "Wiki-контент скопирован: text/html (copy-event)" : "Wiki-контент скопирован: text/html (async clipboard)", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const range = document.createRange(); range.selectNodeContents(el.layoutPreview); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); const ok = document.execCommand("copy"); selection.removeAllRanges(); setStatus(ok ? "Preview скопирован через выделение" : `Не удалось скопировать rich HTML: ${error.message}`, ok ? "ok" : "bad"); if (ok) flashCopySuccess(event && event.currentTarget); } } function buildLayoutPromptText() { const rules = LLM_PROMPT_TEXT.replace(/\s+$/, ""); const source = el.layoutPromptSource.value.trim(); if (!source) { return rules; } return `${rules} --- Преобразуй приведённый ниже исходный текст в layout JSON по правилам выше. <исходный_текст> ${source} </исходный_текст> `; } function updateLayoutPromptControls() { const hasText = el.layoutPromptSource.value.trim().length > 0; el.copyLayoutPrompt.disabled = !hasText; el.copyLayoutPrompt.classList.toggle("btn-primary", hasText); el.layoutLlmLinks.forEach((button) => { button.disabled = !hasText; }); } function openLayoutPromptModal() { updateLayoutPromptControls(); el.layoutPromptModal.hidden = false; el.layoutPromptSource.focus(); } function closeLayoutPromptModal() { const wasOpen = !el.layoutPromptModal.hidden; el.layoutPromptModal.hidden = true; if (wasOpen) el.buildLayoutPromptFromText.focus(); } function copyLayoutPromptText(event) { if (!el.layoutPromptSource.value.trim()) { setStatus("Вставьте исходный текст, чтобы собрать промт", "warn"); return false; } const prompt = buildLayoutPromptText(); const ok = copyTextWithFallback(prompt, `Промт скопирован (${prompt.length} символов)`, "Не удалось скопировать промт"); if (ok && event) flashCopySuccess(event.currentTarget); return ok; } function copyLayoutPromptAndOpen(url, button) { const copied = copyLayoutPromptText(); if (!copied) return; if (button) flashCopySuccess(button, { label: "Скопировано ✓ ↗", duration: 400 }); setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), 420); setStatus("Промт скопирован, открываю LLM-чат", "ok"); } el.buildLayoutPromptFromText.addEventListener("click", openLayoutPromptModal); el.layoutPromptSource.addEventListener("input", updateLayoutPromptControls); el.copyLayoutPrompt.addEventListener("click", copyLayoutPromptText); el.closeLayoutPrompt.addEventListener("click", closeLayoutPromptModal); el.layoutPromptModal.addEventListener("click", (event) => { if (event.target instanceof HTMLElement && event.target.dataset.modalClose === "true") { closeLayoutPromptModal(); } }); document.querySelectorAll(".layout-llm-link").forEach((button) => { const url = button.dataset.layoutLlmUrl; if (!url) return; button.addEventListener("click", () => { copyLayoutPromptAndOpen(url, button); }); }); function openMeetProtocolPromptModal() { const config = getMeetPromptConfig(); el.meetProtocolPromptText.textContent = config.text; document.getElementById("meetProtocolPromptModalTitle").textContent = `Промт: ${config.label}`; el.meetProtocolPromptModal.hidden = false; } function closeMeetProtocolPromptModal() { el.meetProtocolPromptModal.hidden = true; } function copyMeetProtocolPromptText() { const config = getMeetPromptConfig(); copyTextWithFallback(config.text, `Промт «${config.label}» скопирован (${config.text.length} символов)`, "Не удалось скопировать промт встречи"); } el.showMeetProtocolPrompt.addEventListener("click", openMeetProtocolPromptModal); el.copyMeetProtocolPrompt.addEventListener("click", copyMeetProtocolPromptText); el.closeMeetProtocolPrompt.addEventListener("click", closeMeetProtocolPromptModal); el.closeMeetProtocolPromptFooter.addEventListener("click", closeMeetProtocolPromptModal); el.meetProtocolPromptModal.addEventListener("click", (event) => { if (event.target instanceof HTMLElement && event.target.dataset.modalClose === "true") { closeMeetProtocolPromptModal(); } }); function openKbUpdatePromptModal() { el.kbUpdatePromptText.textContent = KB_UPDATE_PROMPT_TEXT; el.kbUpdatePromptModal.hidden = false; } function closeKbUpdatePromptModal() { el.kbUpdatePromptModal.hidden = true; } function copyKbUpdatePromptText() { copyTextWithFallback(KB_UPDATE_PROMPT_TEXT, `Промт базы знаний скопирован (${KB_UPDATE_PROMPT_TEXT.length} символов)`, "Не удалось скопировать промт базы знаний"); } el.showKbUpdatePrompt.addEventListener("click", openKbUpdatePromptModal); el.copyKbUpdatePrompt.addEventListener("click", copyKbUpdatePromptText); el.closeKbUpdatePrompt.addEventListener("click", closeKbUpdatePromptModal); el.closeKbUpdatePromptFooter.addEventListener("click", closeKbUpdatePromptModal); el.kbUpdatePromptModal.addEventListener("click", (event) => { if (event.target instanceof HTMLElement && event.target.dataset.modalClose === "true") { closeKbUpdatePromptModal(); } }); function openGoalExtractionPromptModal() { el.goalExtractionPromptText.textContent = GOAL_EXTRACTION_PROMPT_TEXT; el.goalExtractionPromptModal.hidden = false; } function closeGoalExtractionPromptModal() { el.goalExtractionPromptModal.hidden = true; } function copyGoalExtractionPromptText() { copyTextWithFallback(GOAL_EXTRACTION_PROMPT_TEXT, `Промт извлечения цели скопирован (${GOAL_EXTRACTION_PROMPT_TEXT.length} символов)`, "Не удалось скопировать промт извлечения цели"); } el.showGoalExtractionPrompt.addEventListener("click", openGoalExtractionPromptModal); el.copyGoalExtractionPrompt.addEventListener("click", copyGoalExtractionPromptText); el.closeGoalExtractionPrompt.addEventListener("click", closeGoalExtractionPromptModal); el.closeGoalExtractionPromptFooter.addEventListener("click", closeGoalExtractionPromptModal); el.goalExtractionPromptModal.addEventListener("click", (event) => { if (event.target instanceof HTMLElement && event.target.dataset.modalClose === "true") { closeGoalExtractionPromptModal(); } }); document.querySelectorAll(".kb-llm-link").forEach((button) => { const url = button.dataset.kbLlmUrl; if (!url) return; button.addEventListener("click", () => { copyKbPromptAndOpen(url, button); }); }); document.querySelectorAll(".goal-extraction-llm-link").forEach((button) => { const url = button.dataset.goalExtractionLlmUrl; if (!url) return; button.addEventListener("click", () => { copyGoalExtractionPromptAndOpen(url, button); }); }); async function copyLayoutHtml(event) { const html = el.layoutHtmlOutput.value; if (!html.trim()) { setStatus("Нет layout HTML для сохранения", "warn"); return; } try { await navigator.clipboard.writeText(html); setStatus("Layout HTML сохранён в буфер", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const ok = copyTextWithFallback(html, "Saved layout HTML selection", `Layout HTML save failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); } } function cleanMeetTranscript(source) { const rawTranscript = String(source || ""); const meetingStartedAt = rawTranscript.match(/^\ufeff?(\d{4}-\d{2}-\d{2})[T\s]+(\d{2}:\d{2}:\d{2})\s+-\s+/m); const videoStartedAt = rawTranscript.match(/^\ufeff?Текстовое представление видео «([\s\S]*?)\s+(\d{2}\.\d{2}\.\d{4}\s+\d{2}:\d{2}:\d{2}\s+[A-ZА-Я]+)»?\./m); const videoTitle = videoStartedAt ? videoStartedAt[1].trim() : ""; const startedAtLabel = meetingStartedAt ? `${meetingStartedAt[1]} ${meetingStartedAt[2]}` : (videoStartedAt ? videoStartedAt[2] : ""); const standaloneTimecodes = /^[ \t]*\d{2}:\d{2}:\d{2}[ \t]*$/m.test(rawTranscript); const cleaned = rawTranscript .replace(/^\ufeff?\d{4}-\d{2}-\d{2}[T\s]+\d{2}:\d{2}:\d{2}\s+-\s+/gm, "") .replace(/^\ufeff?Текстовое представление видео [^\n]*\n?/m, "") .replace(standaloneTimecodes ? /^[ \t]*\d{2}:\d{2}:\d{2}[ \t]*\r?\n?/gm : /^$/, "") .replace(/\s*\(распознано\):/g, ":") .trim(); const metadata = [ startedAtLabel ? `Дата и время встречи: ${startedAtLabel}` : "", videoTitle ? `Название встречи: ${videoTitle}` : "", videoStartedAt ? "Формат транскрибации: хронологическое общение участников встречи без разметки спикеров." : "" ].filter(Boolean).join("\n"); return metadata ? `${metadata}\n\n${cleaned}` : cleaned; } function prepareMeetProtocolPrompt() { const cleanedTranscript = cleanMeetTranscript(el.meetTranscriptInput.value); if (!cleanedTranscript) { el.meetProtocolOutput.value = ""; el.meetProtocolMeta.textContent = "Промт ещё не собран"; setPromptOutputEmpty(el.meetProtocolOutput, el.meetProtocolEmpty); setStatus("Нет транскрибации для сборки промта", "warn"); return; } const config = getMeetPromptConfig(); const prompt = config.text.trim(); const output = prompt.includes("[вставь текст встречи]") ? prompt.replace("[вставь текст встречи]", cleanedTranscript) : `${prompt}\n\n# Текст встречи\n\n<meeting_transcript>\n${cleanedTranscript}\n</meeting_transcript>`; const sourceLines = el.meetTranscriptInput.value.split(/\r?\n/).filter((line) => line.trim()).length; const cleanedLines = cleanedTranscript.split(/\r?\n/).filter((line) => line.trim()).length; el.meetProtocolOutput.value = output; el.meetProtocolMeta.textContent = `${config.label}: строк после очистки ${cleanedLines} из ${sourceLines}, символов ${output.length}`; setPromptOutputEmpty(el.meetProtocolOutput, el.meetProtocolEmpty); setStatus(`Полный промт собран: ${config.label}`, "ok"); } async function copyMeetProtocolOutput(event) { if (!el.meetProtocolOutput.value.trim()) { prepareMeetProtocolPrompt(); } if (!el.meetProtocolOutput.value.trim()) { setStatus("Нет собранного промта для копирования", "warn"); return false; } try { await navigator.clipboard.writeText(el.meetProtocolOutput.value); setStatus("Полный промт скопирован", "ok"); flashCopySuccess(event && event.currentTarget); return true; } catch (error) { const ok = copyTextWithFallback(el.meetProtocolOutput.value, "Copied full prompt selection", `Full prompt copy failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); return ok; } } async function copyMeetProtocolAndOpen(url, button) { const copied = await copyMeetProtocolOutput(); if (!copied) return; if (button) flashCopySuccess(button, { label: "Скопировано ✓ ↗", duration: 400 }); setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), 420); setStatus("Промт скопирован, открываю LLM-чат", "ok"); } function pasteHtmlAsMarkdown(event, textarea) { const clipboardData = event.clipboardData; if (!clipboardData) return; const html = clipboardData.getData("text/html"); if (!html || !/<\w/.test(html)) return; // let textarea handle plain text natively event.preventDefault(); const markdown = htmlToMarkdown(html); const start = textarea.selectionStart ?? textarea.value.length; const end = textarea.selectionEnd ?? textarea.value.length; textarea.setRangeText(markdown, start, end, "end"); } function assembleKbPrompt() { const baseValue = el.kbCurrentInput.value; const protocolValue = el.kbProtocolInput.value; if (!protocolValue.trim()) { el.kbPromptOutput.value = ""; el.kbMeta.textContent = "Протокол не задан"; setPromptOutputEmpty(el.kbPromptOutput, el.kbPromptEmpty); return; } const prompt = KB_UPDATE_PROMPT_TEXT.replace(/\s+$/, ""); const output = `${prompt} <ТЕКУЩАЯ_БАЗА_ЗНАНИЙ> ${baseValue} </ТЕКУЩАЯ_БАЗА_ЗНАНИЙ> <НОВЫЙ_ПРОТОКОЛ_ВСТРЕЧИ> ${protocolValue} </НОВЫЙ_ПРОТОКОЛ_ВСТРЕЧИ> `; el.kbPromptOutput.value = output; const baseLines = baseValue.split(/\r?\n/).filter((line) => line.trim()).length; const protocolLines = protocolValue.split(/\r?\n/).filter((line) => line.trim()).length; el.kbMeta.textContent = `${baseLines} lines базы, ${protocolLines} lines протокола, ${output.length} characters`; setPromptOutputEmpty(el.kbPromptOutput, el.kbPromptEmpty); } function buildKbPrompt() { assembleKbPrompt(); if (!el.kbPromptOutput.value.trim()) { setStatus("Нет протокола встречи для сборки промта", "warn"); return; } setStatus("Промт обновления базы знаний собран", "ok"); } async function prepareKbPrompt(event) { assembleKbPrompt(); if (!el.kbPromptOutput.value.trim()) { setStatus("Нет протокола встречи для сборки промта", "warn"); return false; } try { await navigator.clipboard.writeText(el.kbPromptOutput.value); setStatus("Промт обновления базы знаний скопирован", "ok"); flashCopySuccess(event && event.currentTarget); return true; } catch (error) { const ok = copyTextWithFallback(el.kbPromptOutput.value, "Copied knowledge-base prompt selection", `Knowledge-base prompt copy failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); return ok; } } async function copyKbPromptAndOpen(url, button) { const copied = await prepareKbPrompt(); if (!copied) return; if (button) flashCopySuccess(button, { label: "Скопировано ✓ ↗", duration: 400 }); setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), 420); setStatus("Промт скопирован, открываю LLM-чат", "ok"); } function assembleGoalExtractionPrompt() { const dataValue = el.goalExtractionDataInput.value; const currentJsonValue = el.goalExtractionJsonInput.value; if (!dataValue.trim()) { el.goalExtractionPromptOutput.value = ""; el.goalExtractionMeta.textContent = "Данные цели не заданы"; setPromptOutputEmpty(el.goalExtractionPromptOutput, el.goalExtractionPromptEmpty); return; } const prompt = GOAL_EXTRACTION_PROMPT_TEXT.replace(/\s+$/, ""); const currentJsonBlock = currentJsonValue.trim() ? `\n\n--- ТЕКУЩИЙ JSON ---\n${currentJsonValue}\n--- КОНЕЦ ТЕКУЩЕГО JSON ---` : ""; // Вставленное больше не сводится к одному источнику: промт сам делит текст на сегменты // (методика, протоколы) и заводит source_ref на каждый. Готовую ручную разметку // источниками пропускаем как есть — она имеет приоритет над авторазбиением. const hasExplicitSources = /^\s*---\s*ИСТОЧНИК:/m.test(dataValue); const dataBlock = hasExplicitSources ? dataValue : `--- ИСХОДНЫЙ ТЕКСТ ---\n${dataValue}\n--- КОНЕЦ ИСХОДНОГО ТЕКСТА ---`; // Модель не знает текущую дату — без неё collected_at и суждения о сроках были бы догадками. const today = new Date().toISOString().slice(0, 10); const output = `${prompt}${currentJsonBlock} Сегодня: ${today} ${dataBlock} `; el.goalExtractionPromptOutput.value = output; const dataLines = dataValue.split(/\r?\n/).filter((line) => line.trim()).length; const jsonLines = currentJsonValue.split(/\r?\n/).filter((line) => line.trim()).length; el.goalExtractionMeta.textContent = `Строк данных: ${dataLines}, строк JSON: ${jsonLines}, символов: ${output.length}`; setPromptOutputEmpty(el.goalExtractionPromptOutput, el.goalExtractionPromptEmpty); } const GOAL_DIFF_KIND_LABEL = { removed: "исчезло", added: "добавлено", changed: "изменено", status: "статус" }; // Сверка двух версий цели: единственный способ поймать, что обновление молча // потеряло кусок данных. Сравнение детерминированное, без обращения к нейросети. function compareGoalVersions() { const parse = (raw, label) => { if (!raw.trim()) throw new Error(`${label} не заполнен`); const value = JSON.parse(raw); if (value?.schema_version !== "goal-template.v1" && !value?.goal_identity) { throw new Error(`${label} не похож на JSON цели`); } return value; }; let previous; let next; try { previous = parse(el.goalExtractionJsonInput.value, "Текущий JSON"); next = parse(el.goalDiffNewJson.value, "Обновлённый JSON"); } catch (error) { el.goalDiffReport.innerHTML = ""; el.goalDiffMeta.textContent = error.message; setStatus(`Сверка невозможна: ${error.message}`, "bad"); return; } const { changes, summary } = diffGoals(previous, next); const order = { removed: 0, status: 1, changed: 2, added: 3 }; const sorted = [...changes].sort((a, b) => (order[a.kind] ?? 9) - (order[b.kind] ?? 9)); const shown = sorted.slice(0, 300); const warnings = []; if (summary.removed) warnings.push(`исчезло значений: ${summary.removed} — проверьте каждое`); if (summary.brokenSources.length) warnings.push(`переписаны источники: ${summary.brokenSources.join("; ")}`); const rows = shown.map((change) => `<tr data-kind="${escapeHtml(change.kind)}">` + `<td>${escapeHtml(GOAL_DIFF_KIND_LABEL[change.kind] ?? change.kind)}</td>` + `<td>${escapeHtml(change.section)}</td>` + `<td>${escapeHtml(change.subject)}</td>` + `<td>${escapeHtml(change.before)}</td>` + `<td>${escapeHtml(change.after)}</td>` + "</tr>").join(""); el.goalDiffReport.innerHTML = changes.length ? `<div class="goal-diff-summary">добавлено: ${summary.added}, изменено: ${summary.changed}, статусов: ${summary.status}, исчезло: ${summary.removed}` + (summary.newSources.length ? `<br>новые источники: ${escapeHtml(summary.newSources.join("; "))}` : "") + (warnings.length ? `<br>внимание: ${escapeHtml(warnings.join("; "))}` : "") + (changes.length > shown.length ? `<br>показаны первые ${shown.length} из ${changes.length}` : "") + "</div>" + `<table><thead><tr><th>Что</th><th>Раздел</th><th>Значение</th><th>Было</th><th>Стало</th></tr></thead><tbody>${rows}</tbody></table>` : '<div class="goal-diff-summary">Версии идентичны: ни одно значение не добавилось, не изменилось и не исчезло.</div>'; el.goalDiffMeta.textContent = `Источников было ${summary.sourcesBefore}, стало ${summary.sourcesAfter}; изменений ${changes.length}`; if (summary.removed || summary.brokenSources.length) { setStatus(`Сверка: ${summary.removed} значений исчезло${summary.brokenSources.length ? ", источники переписаны" : ""}`, "warn"); } else { setStatus(`Сверка: ${summary.added} добавлено, ${summary.changed} изменено, потерь нет`, "ok"); } } function buildGoalExtractionPrompt() { assembleGoalExtractionPrompt(); if (!el.goalExtractionPromptOutput.value.trim()) { setStatus("Нет данных цели для сборки промта", "warn"); return; } setStatus("Промт извлечения цели собран", "ok"); } async function prepareGoalExtractionPrompt(event) { assembleGoalExtractionPrompt(); if (!el.goalExtractionPromptOutput.value.trim()) { setStatus("Нет данных цели для сборки промта", "warn"); return false; } try { await navigator.clipboard.writeText(el.goalExtractionPromptOutput.value); setStatus("Промт извлечения цели скопирован", "ok"); flashCopySuccess(event && event.currentTarget); return true; } catch (error) { const ok = copyTextWithFallback(el.goalExtractionPromptOutput.value, "Copied goal extraction prompt selection", `Goal extraction prompt copy failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); return ok; } } async function copyGoalExtractionPromptAndOpen(url, button) { const copied = await prepareGoalExtractionPrompt(); if (!copied) return; if (button) flashCopySuccess(button, { label: "Скопировано ✓ ↗", duration: 400 }); setTimeout(() => window.open(url, "_blank", "noopener,noreferrer"), 420); setStatus("Промт скопирован, открываю LLM-чат", "ok"); } function convertHtmlToMarkdown() { const html = el.markdownHtmlInput.value; const markdown = htmlToMarkdown(html); el.markdownOutput.value = markdown; el.markdownMeta.textContent = markdown ? `Строк Markdown: ${markdown.split(/\r?\n/).length}` : "Markdown не получен"; setPromptOutputEmpty(el.markdownOutput, el.markdownOutputEmpty); setStatus(markdown ? "HTML сконвертирован в Markdown" : "Markdown не получен", markdown ? "ok" : "warn"); } function loadMarkdownClipboardPayload(clipboardData) { const html = clipboardData?.getData("text/html") || ""; const plain = clipboardData?.getData("text/plain") || ""; const source = html || plain; el.markdownHtmlInput.value = html ? formatHtml(html) : plain; el.markdownOutput.value = htmlToMarkdown(source, plain); const types = clipboardData ? Array.from(clipboardData.types || []) : []; el.markdownMeta.textContent = `${types.join(", ") || "plain input"}; строк: ${el.markdownOutput.value.split(/\r?\n/).filter(Boolean).length}`; setPromptOutputEmpty(el.markdownOutput, el.markdownOutputEmpty); setStatus(html ? "Вставленный text/html сконвертирован в Markdown" : "Вставленный text/plain сконвертирован в Markdown", html ? "ok" : "warn"); } async function copyMarkdownOutput(event) { try { await navigator.clipboard.writeText(el.markdownOutput.value); setStatus("Markdown скопирован", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const ok = copyTextWithFallback(el.markdownOutput.value, "Copied Markdown selection", `Markdown copy failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); } } function convertMdToWiki() { const source = el.mdToWikiInput.value; let html; try { html = markdownToWikiHtml(source); } catch (error) { setPreviewEmpty(el.mdToWikiPreview, true); el.mdToWikiMeta.textContent = `Ошибка конвертации: ${error.message}`; setStatus(`Ошибка конвертации Markdown в wiki: ${error.message}`, "bad"); return; } renderIntoPreview(el.mdToWikiPreview, html); state.mdToWikiHtml = html; el.mdToWikiHtmlOutput.value = formatHtml(html); const blockCount = (html.match(/<(?:h[1-6]|p|ul|ol|hr|pre|blockquote|section)\b/g) || []).length; const dataTypes = Array.from(new Set(Array.from(html.matchAll(/data-type="([^"]+)"/g)).map((match) => match[1]))).sort(); el.mdToWikiMeta.textContent = source.trim() ? `${blockCount} top-level blocks, data-types: ${dataTypes.join(", ") || "none"}` : "No markdown converted"; setStatus(source.trim() ? "Markdown сконвертирован в wiki-HTML" : "Нет Markdown для конвертации", source.trim() ? "ok" : "warn"); } function loadMdToWikiProtocolExample() { el.mdToWikiInput.value = PROTOCOL_EXAMPLE_MARKDOWN_TEXT; convertMdToWiki(); } async function copyMdToWikiRich(event) { const html = state.mdToWikiHtml || el.mdToWikiHtmlOutput.value; if (!html.trim()) { setStatus("Нет wiki-контента для копирования", "warn"); return; } try { // Same reason as copyLayoutRich: text/html alone, written verbatim. const verbatim = writeClipboardViaCopyEvent(html, null); if (!verbatim) { await navigator.clipboard.write([ new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }) }) ]); } setStatus(verbatim ? "Wiki-HTML скопирован: text/html (copy-event)" : "Wiki-HTML скопирован: text/html (async clipboard)", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const range = document.createRange(); range.selectNodeContents(el.mdToWikiPreview); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); const ok = document.execCommand("copy"); selection.removeAllRanges(); setStatus(ok ? "Wiki-HTML скопирован через выделение" : `Не удалось скопировать wiki-HTML: ${error.message}`, ok ? "ok" : "bad"); if (ok) flashCopySuccess(event && event.currentTarget); } } async function saveMdToWikiHtml(event) { const html = el.mdToWikiHtmlOutput.value; if (!html.trim()) { setStatus("Нет wiki-HTML для сохранения", "warn"); return; } try { await navigator.clipboard.writeText(html); setStatus("Wiki-HTML сохранён в буфер", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { const ok = copyTextWithFallback(html, "Saved Markdown to Wiki HTML selection", `Save HTML failed: ${error.message}`); if (ok) flashCopySuccess(event && event.currentTarget); } } function applyHtmlToEditor(html) { state.currentHtml = html || ""; el.editor.innerHTML = state.currentHtml; el.htmlEditor.value = formatHtml(html); state.currentPlain = el.editor.innerText; el.plainText.value = state.currentPlain; refreshPayloadDump(); updateMeta(); } function syncFromEditor() { state.currentHtml = el.editor.innerHTML; state.currentPlain = el.editor.innerText; el.htmlEditor.value = formatHtml(state.currentHtml); el.plainText.value = state.currentPlain; refreshPayloadDump(); updateMeta(); } function formatHtmlEditor() { const wasHtmlMode = !el.htmlEditor.classList.contains("hidden"); const html = wasHtmlMode ? el.htmlEditor.value : state.currentHtml || getCurrentHtml(); el.editor.classList.add("hidden"); el.htmlEditor.classList.remove("hidden"); el.visualTab.setAttribute("aria-pressed", "false"); el.htmlTab.setAttribute("aria-pressed", "true"); el.htmlEditor.value = formatHtml(html); state.currentHtml = el.htmlEditor.value; state.lastEditedSurface = "html"; refreshPayloadDump(); updateMeta(); setStatus("HTML отформатирован", "ok"); } function loadClipboardPayload(clipboardData) { const types = clipboardData ? Array.from(clipboardData.types || []) : []; const html = clipboardData ? clipboardData.getData("text/html") : ""; const plain = clipboardData ? clipboardData.getData("text/plain") : ""; const sourceUrl = clipboardData ? clipboardData.getData("chromium/x-source-url") : ""; state.types = types; state.originalHtml = html; state.originalPlain = plain; state.sourceUrl = sourceUrl; el.originalHtml.value = formatHtml(html); const editableHtml = html || `<pre>${escapeHtml(plain)}</pre>`; applyHtmlToEditor(editableHtml); state.lastEditedSurface = "visual"; setStatus(html ? "Вставлен rich HTML" : "Вставлен только текст", html ? "ok" : "warn"); } function selectEditorContents() { setMode("visual"); const range = document.createRange(); range.selectNodeContents(el.editor); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); el.editor.focus(); } async function copyPayload(event) { const payload = el.payloadDump.value || buildPayloadDump(); try { await navigator.clipboard.writeText(payload); setStatus("Payload скопирован", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { el.payloadDump.focus(); el.payloadDump.select(); const ok = document.execCommand("copy"); setStatus(ok ? "Payload скопирован через выделение" : `Не удалось скопировать payload: ${error.message}`, ok ? "ok" : "bad"); if (ok) flashCopySuccess(event && event.currentTarget); } } async function copyRich(event) { const html = getCurrentHtml(); const plain = getCurrentPlainText(); if (!html.trim() && !plain.trim()) { setStatus("Нечего копировать", "warn"); return; } try { // Same reason as copyLayoutRich: keep the payload verbatim for the wiki editor. if (!writeClipboardViaCopyEvent(html, plain)) { await navigator.clipboard.write([ new ClipboardItem({ "text/html": new Blob([html], { type: "text/html" }), "text/plain": new Blob([plain], { type: "text/plain" }) }) ]); } setStatus("Скопированы text/html и text/plain", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { selectEditorContents(); const ok = document.execCommand("copy"); setStatus(ok ? "Скопирован выделенный HTML" : `Не удалось скопировать rich HTML: ${error.message}`, ok ? "ok" : "bad"); if (ok) flashCopySuccess(event && event.currentTarget); } } function clearAll() { state.originalHtml = ""; state.originalPlain = ""; state.currentHtml = ""; state.currentPlain = ""; state.types = []; state.sourceUrl = ""; state.lastEditedSurface = null; el.pasteZone.innerHTML = ""; el.originalHtml.value = ""; el.payloadDump.value = ""; el.plainText.value = ""; el.editor.innerHTML = ""; el.htmlEditor.value = ""; updateMeta(); setStatus("Очищено"); } el.pasteZone.addEventListener("paste", (event) => { event.preventDefault(); loadClipboardPayload(event.clipboardData); }); el.editor.addEventListener("input", () => { state.currentHtml = el.editor.innerHTML; state.currentPlain = el.editor.innerText; el.plainText.value = state.currentPlain; state.lastEditedSurface = "visual"; refreshPayloadDump(); updateMeta(); }); el.htmlEditor.addEventListener("input", () => { state.currentHtml = el.htmlEditor.value; const scratch = document.createElement("div"); scratch.innerHTML = state.currentHtml; state.currentPlain = scratch.innerText; el.plainText.value = state.currentPlain; state.lastEditedSurface = "html"; refreshPayloadDump(); updateMeta(); }); el.plainText.addEventListener("input", () => { state.currentPlain = el.plainText.value; state.lastEditedSurface = "plain"; refreshPayloadDump(); updateMeta(); }); el.applyRaw.addEventListener("click", () => { applyHtmlToEditor(el.htmlEditor.value); state.lastEditedSurface = "visual"; setMode("visual"); setStatus("HTML применён к редактору", "ok"); }); el.syncRaw.addEventListener("click", () => { syncFromEditor(); state.lastEditedSurface = "html"; setStatus("Синхронизировано из редактора", "ok"); }); el.copyPayload.addEventListener("click", (event) => copyPayload(event)); el.copyRich.addEventListener("click", (event) => copyRich(event)); el.clearAll.addEventListener("click", clearAll); el.visualTab.addEventListener("click", () => setMode("visual")); el.htmlTab.addEventListener("click", () => setMode("html")); el.formatHtmlEditor.addEventListener("click", formatHtmlEditor); el.toolTabs.forEach((tab) => { tab.addEventListener("click", (event) => { if (event.target.closest(".tab-info")) { toggleTabInfoPopover(tab); return; } activateToolTab(tab.dataset.toolTab); }); }); el.mobileToolSelect.addEventListener("change", () => { activateToolTab(el.mobileToolSelect.value); }); el.activeToolInfo.addEventListener("click", () => { toggleTabInfoPopover(el.activeToolInfo); }); el.clearExcelInput.addEventListener("click", () => { state.excelSourceHtml = ""; el.excelPasteZone.innerHTML = ""; el.excelOutput.value = ""; el.excelMeta.textContent = "Таблица ещё не сконвертирована"; setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); setStatus("Вкладка WIKI to Excel очищена"); }); el.excelPasteZone.addEventListener("paste", (event) => { event.preventDefault(); loadExcelClipboardPayload(event.clipboardData); }); el.normalizeExcel.addEventListener("click", normalizeExcelInput); el.copyExcelOutput.addEventListener("click", (event) => copyExcelOutput(event)); el.clearLayout.addEventListener("click", () => { el.layoutJsonInput.value = ""; el.layoutHtmlOutput.value = ""; state.layoutHtml = ""; renderIntoPreview(el.layoutPreview, ""); el.layoutMeta.textContent = "Layout ещё не отрендерен"; setStatus("Вкладка Layout JSON очищена"); }); function showBoardWarnings(warnings) { if (!warnings.length) { el.boardWarnings.textContent = "Предупреждений нет"; return; } el.boardWarnings.innerHTML = ""; const list = document.createElement("ul"); for (const warning of warnings) { const item = document.createElement("li"); const where = warning.path ? ` — ${warning.path}` : ""; const what = warning.detail ? `: ${warning.detail}` : ""; item.textContent = `[${warning.severity}] ${warning.code}${where}${what}`; list.appendChild(item); } el.boardWarnings.appendChild(list); } // Поле принимает три вида JSON, и вид определяется по содержимому: // сцена (есть nodes), board-layout-v1 от LLM (есть content) и нативная // вставка с доски (есть tree, возможно внутри обёртки буфера). function boardInputToScene(text) { let parsed = null; try { parsed = JSON.parse(text); } catch (error) { throw new Error(`это не JSON: ${error.message}`); } if (Array.isArray(parsed?.nodes)) { return { scene: parsed, warnings: [], kind: "сцена" }; } if (parsed?.content || parsed?.$schema === "board-layout-v1") { const { scene, warnings } = boardLayoutToScene(parsed); return { scene, warnings, kind: "board-layout-v1" }; } const { scene, warnings } = parseBoardPayload(parsed); return { scene, warnings, kind: "вставка с доски" }; } // «Разобрать» всегда приводит поле к board-layout-v1: это тот формат, который // понимает промпт и в котором LLM может править доску, не считая пиксели. function parseBoardIntoScene() { try { const { scene, warnings, kind } = boardInputToScene(el.boardSceneInput.value); const all = [...warnings]; let layout; if (kind === "board-layout-v1") { layout = JSON.parse(el.boardSceneInput.value); } else { const reverse = sceneToBoardLayout(scene); layout = reverse.layout; all.push(...reverse.warnings); } el.boardSceneInput.value = JSON.stringify(layout, null, 2); showBoardWarnings(all); setStatus( `Разобрано (${kind}): ${scene.nodes.length} элементов, ${scene.edges.length} связей — в поле board-layout-v1`, "ok" ); } catch (error) { showBoardWarnings(error.errors ?? []); setStatus(`Не разобрать: ${error.message}`, "bad"); } } async function copyBoardPayloadToClipboard() { let payload; try { const { scene, warnings } = boardInputToScene(el.boardSceneInput.value); showBoardWarnings(warnings); payload = sceneToBoardPayload(scene); } catch (error) { showBoardWarnings(error.errors ?? []); setStatus(`Доска не собирается: ${error.message}`, "bad"); return; } // Доска читает только text/plain, поэтому html === null: второй MIME ей не нужен. if (writeClipboardViaCopyEvent(null, payload)) { setStatus("Payload доски скопирован: text/plain (copy-event)", "ok"); return; } try { await navigator.clipboard.writeText(payload); setStatus("Payload доски скопирован: text/plain (async clipboard)", "ok"); } catch (error) { setStatus(`Не скопировать: ${error.message}`, "bad"); } } function buildBoardPromptText() { const rules = BOARD_PROMPT_TEXT.replace(/\s+$/, ""); const source = el.boardPromptSource.value.trim(); if (!source) return rules; return `${rules} --- Собери доску из приведённого ниже исходного материала по правилам выше. <исходный_материал> ${source} </исходный_материал> `; } function updateBoardPromptControls() { const hasText = el.boardPromptSource.value.trim().length > 0; el.copyBoardPrompt.disabled = !hasText; el.copyBoardPrompt.classList.toggle("btn-primary", hasText); el.boardLlmLinks.forEach((button) => { button.disabled = !hasText; }); } function openBoardPromptModal() { updateBoardPromptControls(); el.boardPromptModal.hidden = false; el.boardPromptSource.focus(); } function closeBoardPromptModal() { const wasOpen = !el.boardPromptModal.hidden; el.boardPromptModal.hidden = true; if (wasOpen) el.buildBoardPromptFromText.focus(); } el.buildBoardPromptFromText.addEventListener("click", openBoardPromptModal); el.closeBoardPrompt.addEventListener("click", closeBoardPromptModal); el.boardPromptModal.addEventListener("click", (event) => { if (event.target.dataset.modalClose === "true") closeBoardPromptModal(); }); el.boardPromptSource.addEventListener("input", updateBoardPromptControls); el.copyBoardPrompt.addEventListener("click", async (event) => { try { await navigator.clipboard.writeText(buildBoardPromptText()); setStatus("Промт для доски скопирован", "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { setStatus(`Не скопировать промт: ${error.message}`, "bad"); } }); el.boardLlmLinks.forEach((button) => { button.addEventListener("click", async () => { try { await navigator.clipboard.writeText(buildBoardPromptText()); setStatus("Промт скопирован, открываю LLM", "ok"); } catch (error) { setStatus(`Промт не скопирован: ${error.message}`, "warn"); } window.open(button.dataset.boardLlmUrl, "_blank", "noopener"); }); }); // Обратная операция: доска → таблицы объектов в Markdown. Ключ-значение // таблицы становятся строками, таблицы одной формы сливаются в одну. async function copyBoardAsMarkdown(event) { let markdown; try { const { scene, warnings, kind } = boardInputToScene(el.boardSceneInput.value); showBoardWarnings(warnings); const layout = kind === "board-layout-v1" ? JSON.parse(el.boardSceneInput.value) : sceneToBoardLayout(scene).layout; markdown = boardLayoutToMarkdown(layout); } catch (error) { showBoardWarnings(error.errors ?? []); setStatus(`Не собрать Markdown: ${error.message}`, "bad"); return; } const rows = markdown.split("\n").filter((line) => line.startsWith("|")).length; try { await navigator.clipboard.writeText(markdown); setStatus(`Markdown скопирован: ${rows} строк таблиц`, "ok"); flashCopySuccess(event && event.currentTarget); } catch (error) { el.boardSceneInput.value = markdown; setStatus(`Буфер недоступен (${error.message}) — Markdown положен в поле слева`, "warn"); } } el.boardToMarkdown.addEventListener("click", copyBoardAsMarkdown); el.parseBoardPayloadBtn.addEventListener("click", parseBoardIntoScene); el.copyBoardPayload.addEventListener("click", copyBoardPayloadToClipboard); el.formatBoardScene.addEventListener("click", () => { try { el.boardSceneInput.value = JSON.stringify(JSON.parse(el.boardSceneInput.value), null, 2); } catch (error) { setStatus(`JSON не разобрать: ${error.message}`, "bad"); } }); el.clearBoard.addEventListener("click", () => { el.boardSceneInput.value = ""; el.boardWarnings.textContent = "Ничего не разобрано"; setStatus("Вкладка JSON to Board очищена"); }); el.boardSceneInput.addEventListener("paste", () => { scheduleAfterPaste(parseBoardIntoScene); }); el.formatLayoutJson.addEventListener("click", formatLayoutJson); el.renderLayoutJson.addEventListener("click", renderLayoutJson); el.loadLayoutExample.addEventListener("click", loadLayoutExample); el.loadKnowledgeBaseExample.addEventListener("click", loadKnowledgeBaseExample); el.loadProtocolExample.addEventListener("click", loadProtocolExample); el.layoutJsonInput.addEventListener("paste", () => { scheduleAfterPaste(renderLayoutJson); }); el.layoutGoalProfile.addEventListener("change", () => { if (el.layoutJsonInput.value.trim()) renderLayoutJson(); }); el.copyLayoutHtml.addEventListener("click", (event) => copyLayoutHtml(event)); el.copyLayoutRich.addEventListener("click", (event) => copyLayoutRich(event)); el.buildMeetProtocol.addEventListener("click", prepareMeetProtocolPrompt); el.meetPromptModeButtons.forEach((button) => { button.addEventListener("click", () => selectMeetPromptMode(button.dataset.meetPromptMode)); }); el.prepareMeetProtocol.addEventListener("click", (event) => copyMeetProtocolOutput(event)); el.clearMeetProtocol.addEventListener("click", () => { el.meetTranscriptInput.value = ""; el.meetProtocolOutput.value = ""; el.meetProtocolMeta.textContent = "Промт ещё не собран"; setPromptOutputEmpty(el.meetProtocolOutput, el.meetProtocolEmpty); setStatus("Вкладка «Протокол встречи» очищена"); }); el.meetTranscriptInput.addEventListener("paste", () => { scheduleAfterPaste(prepareMeetProtocolPrompt); }); el.buildKbPrompt.addEventListener("click", buildKbPrompt); el.prepareKbPrompt.addEventListener("click", (event) => prepareKbPrompt(event)); el.kbCurrentInput.addEventListener("paste", (event) => { pasteHtmlAsMarkdown(event, el.kbCurrentInput); scheduleAfterPaste(assembleKbPrompt); }); el.kbProtocolInput.addEventListener("paste", (event) => { pasteHtmlAsMarkdown(event, el.kbProtocolInput); scheduleAfterPaste(assembleKbPrompt); }); el.kbCurrentInput.addEventListener("input", assembleKbPrompt); el.kbProtocolInput.addEventListener("input", assembleKbPrompt); el.clearKb.addEventListener("click", () => { el.kbCurrentInput.value = ""; el.kbProtocolInput.value = ""; el.kbPromptOutput.value = ""; el.kbMeta.textContent = "Промт ещё не собран"; setPromptOutputEmpty(el.kbPromptOutput, el.kbPromptEmpty); setStatus("Вкладка «База знаний» очищена"); }); el.buildGoalExtractionPrompt.addEventListener("click", buildGoalExtractionPrompt); el.prepareGoalExtractionPrompt.addEventListener("click", (event) => prepareGoalExtractionPrompt(event)); el.goalExtractionDataInput.addEventListener("paste", () => { scheduleAfterPaste(assembleGoalExtractionPrompt); }); el.goalExtractionJsonInput.addEventListener("paste", () => { scheduleAfterPaste(assembleGoalExtractionPrompt); }); el.goalExtractionDataInput.addEventListener("input", assembleGoalExtractionPrompt); el.goalExtractionJsonInput.addEventListener("input", assembleGoalExtractionPrompt); el.compareGoalVersions.addEventListener("click", compareGoalVersions); el.goalDiffNewJson.addEventListener("paste", () => { scheduleAfterPaste(compareGoalVersions); }); el.copyGoalDiffJson.addEventListener("click", (event) => { if (!el.goalDiffNewJson.value.trim()) { setStatus("Обновлённый JSON не заполнен", "warn"); return; } const ok = copyTextWithFallback(el.goalDiffNewJson.value, "Обновлённый JSON цели скопирован", "Не удалось скопировать JSON цели"); if (ok) flashCopySuccess(event.currentTarget); }); el.clearGoalExtraction.addEventListener("click", () => { el.goalExtractionDataInput.value = ""; el.goalExtractionJsonInput.value = ""; el.goalExtractionPromptOutput.value = ""; el.goalExtractionMeta.textContent = "Промт ещё не собран"; el.goalDiffNewJson.value = ""; el.goalDiffReport.innerHTML = ""; el.goalDiffMeta.textContent = "Сверка не запускалась"; setPromptOutputEmpty(el.goalExtractionPromptOutput, el.goalExtractionPromptEmpty); setStatus("Вкладка «Цель» очищена"); }); document.querySelectorAll(".meet-llm-link").forEach((button) => { button.addEventListener("click", () => { copyMeetProtocolAndOpen(button.dataset.meetLlmUrl, button); }); }); el.convertMarkdown.addEventListener("click", convertHtmlToMarkdown); el.copyMarkdown.addEventListener("click", (event) => copyMarkdownOutput(event)); el.clearMarkdown.addEventListener("click", () => { el.markdownHtmlInput.value = ""; el.markdownOutput.value = ""; el.markdownMeta.textContent = "HTML ещё не сконвертирован"; setPromptOutputEmpty(el.markdownOutput, el.markdownOutputEmpty); setStatus("Вкладка HTML to Markdown очищена"); }); el.clearMdToWiki.addEventListener("click", () => { el.mdToWikiInput.value = ""; renderIntoPreview(el.mdToWikiPreview, ""); el.mdToWikiHtmlOutput.value = ""; state.mdToWikiHtml = ""; el.mdToWikiMeta.textContent = "Markdown ещё не сконвертирован"; setStatus("Вкладка Markdown to HTML очищена"); }); el.convertMdToWiki.addEventListener("click", convertMdToWiki); el.loadMdToWikiProtocolExample.addEventListener("click", loadMdToWikiProtocolExample); el.mdToWikiInput.addEventListener("paste", () => { scheduleAfterPaste(convertMdToWiki); }); el.saveMdToWikiHtml.addEventListener("click", (event) => saveMdToWikiHtml(event)); el.copyMdToWikiRich.addEventListener("click", (event) => copyMdToWikiRich(event)); el.markdownHtmlInput.addEventListener("paste", (event) => { if (event.clipboardData?.types?.some((type) => type === "text/html" || type === "text/plain")) { event.preventDefault(); loadMarkdownClipboardPayload(event.clipboardData); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !el.layoutPromptModal.hidden) { closeLayoutPromptModal(); return; } if (event.key === "Escape" && !el.meetProtocolPromptModal.hidden) { closeMeetProtocolPromptModal(); return; } if (event.key === "Escape" && !el.kbUpdatePromptModal.hidden) { closeKbUpdatePromptModal(); return; } if (event.key === "Escape" && !el.goalExtractionPromptModal.hidden) { closeGoalExtractionPromptModal(); return; } if (!(event.ctrlKey || event.metaKey)) return; if (event.key === "Enter") { const activeTab = el.toolTabs.find((tab) => tab.getAttribute("aria-selected") === "true"); const tabName = activeTab?.dataset.toolTab; const action = { clipboard: copyRich, excel: normalizeExcelInput, layout: renderLayoutJson, board: parseBoardIntoScene, "meet-protocol": prepareMeetProtocolPrompt, "knowledge-base": prepareKbPrompt, "goal-extraction": prepareGoalExtractionPrompt, markdown: convertHtmlToMarkdown, "md-to-wiki": convertMdToWiki }[tabName]; if (!action) return; event.preventDefault(); action(); } }); function hideTabInfoPopover() { el.tabInfoPopover.hidden = true; for (const button of el.tabInfoButtons) { button.setAttribute("aria-expanded", "false"); } } function showTabInfoPopover(button) { const key = button.dataset.toolTab; const entry = TAB_DESCRIPTIONS[key]; if (!entry) return; el.tabInfoPopoverTitle.textContent = entry.title; el.tabInfoPopoverBody.textContent = entry.body; for (const other of el.tabInfoButtons) { other.setAttribute("aria-expanded", String(other === button)); } el.tabInfoPopover.hidden = false; const rect = button.getBoundingClientRect(); const popover = el.tabInfoPopover; const popoverWidth = popover.offsetWidth || 320; const viewportRight = document.documentElement.clientWidth - 8; let left = rect.left; if (left + popoverWidth > viewportRight) { left = Math.max(8, viewportRight - popoverWidth); } popover.style.top = `${rect.bottom + 6}px`; popover.style.left = `${left}px`; } function toggleTabInfoPopover(button) { const isOpen = button.getAttribute("aria-expanded") === "true" && !el.tabInfoPopover.hidden; if (isOpen) { hideTabInfoPopover(); } else { showTabInfoPopover(button); } } document.addEventListener("mousedown", (event) => { if (el.tabInfoPopover.hidden) return; if (el.tabInfoPopover.contains(event.target)) return; if (event.target.closest(".tab-info, [data-active-tool-info]")) return; hideTabInfoPopover(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !el.tabInfoPopover.hidden) { hideTabInfoPopover(); } }); window.addEventListener("resize", hideTabInfoPopover); window.addEventListener("scroll", hideTabInfoPopover, true); initMobilePaneTabs(); try { const storedTab = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY); const initialTab = storedTab && Object.prototype.hasOwnProperty.call(el.toolViews, storedTab) ? storedTab : DEFAULT_TOOL_TAB; activateToolTab(initialTab, { persist: false }); } catch { activateToolTab(DEFAULT_TOOL_TAB, { persist: false }); } setPromptOutputEmpty(el.excelOutput, el.excelOutputEmpty); updateMeetPromptModeUi(); setPromptOutputEmpty(el.meetProtocolOutput, el.meetProtocolEmpty); setPromptOutputEmpty(el.kbPromptOutput, el.kbPromptEmpty); setPromptOutputEmpty(el.goalExtractionPromptOutput, el.goalExtractionPromptEmpty); setPromptOutputEmpty(el.markdownOutput, el.markdownOutputEmpty); updateMeta(); </script> </body> </html>