Clarify XML AI structure flow in HTML5 UI
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 15:02:26 +03:00
parent 4c02e2f73a
commit d8394e4e89
3 changed files with 87 additions and 0 deletions
@@ -32,6 +32,7 @@ def render_html5_ai_structure_page(
<section class="panel setup-main"> <section class="panel setup-main">
<div class="panel-title">Подготовка структуры</div> <div class="panel-title">Подготовка структуры</div>
{render_html5_ai_structure_agent_panel(project_id, agent_info=agent_info)} {render_html5_ai_structure_agent_panel(project_id, agent_info=agent_info)}
{render_html5_ai_structure_source_hint()}
{render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)} {render_html5_ai_structure_form(project_id, saved_credentials=saved_credentials)}
<div data-html5-ai-structure-path-check>{render_html5_ai_structure_path_check(None)}</div> <div data-html5-ai-structure-path-check>{render_html5_ai_structure_path_check(None)}</div>
<p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p> <p class="object-summary">Пути должны быть доступны серверу SFERA/API. Для docker-test используйте папку, смонтированную или доступную внутри контейнера; локальные диски Windows и закрытые SMB-папки без учетных данных сервер не увидит.</p>
@@ -77,6 +78,35 @@ def render_html5_ai_structure_agent_panel(project_id: str, *, agent_info: dict[s
""" """
def render_html5_ai_structure_source_hint() -> str:
return """
<section class="ai-structure-hint">
<div class="access-plan-head">
<span class="status-pill">xml/xdt</span>
<strong>Ожидаемый формат выгрузки 1С</strong>
</div>
<div class="ai-structure-hint-grid">
<article class="access-card">
<strong>Основная конфигурация</strong>
<small>Во входной папке должна быть папка <code>Конфигурация</code> с XML/BSL/MDO-файлами выгрузки 1С.</small>
</article>
<article class="access-card">
<strong>Расширения</strong>
<small>Каждое расширение кладите в отдельную соседнюю папку с его именем. SFERA загрузит их как части одного проекта.</small>
</article>
<article class="access-card">
<strong>Без лишнего сырья</strong>
<small>Для Codex будет собран compact-пакет: краткие индексы, brief-контекст, layout проекта и выборочные исходники вместо полного дубля всей выгрузки.</small>
</article>
</div>
<ul class="access-warnings">
<li>Если на входе уже XML-выгрузка, Windows Agent не нужен: сервер сам строит NormalizedProject, SIR и пакет для Codex.</li>
<li>Agent нужен только для бинарных <code>.cf</code>/<code>.cfe</code>, когда сначала надо получить XML-структуру из платформы 1С.</li>
</ul>
</section>
"""
def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[str, str] | None = None) -> str: def render_html5_ai_structure_form(project_id: str, *, saved_credentials: dict[str, str] | None = None) -> str:
saved_credentials = saved_credentials or {} saved_credentials = saved_credentials or {}
saved_username = str(saved_credentials.get("username") or "") saved_username = str(saved_credentials.get("username") or "")
@@ -178,6 +208,7 @@ def render_html5_ai_structure_result(result: dict | None) -> str:
artifacts = list(result.get("artifacts") or []) artifacts = list(result.get("artifacts") or [])
snapshot = result.get("snapshot") or {} snapshot = result.get("snapshot") or {}
normalized = result.get("normalized") or {} normalized = result.get("normalized") or {}
source_layout = result.get("source_layout") or {}
status = _status_text(result.get("status")) status = _status_text(result.get("status"))
return f""" return f"""
<section class="ai-structure-result" data-html5-ai-structure-status="{escape(str(result.get('status', '')))}"> <section class="ai-structure-result" data-html5-ai-structure-status="{escape(str(result.get('status', '')))}">
@@ -192,6 +223,7 @@ def render_html5_ai_structure_result(result: dict | None) -> str:
<div><dt>Связи</dt><dd>{escape(str(snapshot.get("edges", 0)))}</dd></div> <div><dt>Связи</dt><dd>{escape(str(snapshot.get("edges", 0)))}</dd></div>
<div><dt>Объекты</dt><dd>{escape(str(normalized.get("objects", 0)))}</dd></div> <div><dt>Объекты</dt><dd>{escape(str(normalized.get("objects", 0)))}</dd></div>
</dl> </dl>
{render_html5_ai_structure_result_summary(source_layout, normalized)}
<div class="panel-title">Артефакты</div> <div class="panel-title">Артефакты</div>
<div class="access-operations">{''.join(f'<article class="access-card"><strong>{escape(_artifact_text(item))}</strong><small>Файл пакета структуры</small></article>' for item in artifacts)}</div> <div class="access-operations">{''.join(f'<article class="access-card"><strong>{escape(_artifact_text(item))}</strong><small>Файл пакета структуры</small></article>' for item in artifacts)}</div>
{_diagnostics(diagnostics)} {_diagnostics(diagnostics)}
@@ -242,6 +274,42 @@ def render_html5_ai_structure_error(message: str) -> str:
""" """
def render_html5_ai_structure_result_summary(source_layout: dict[str, object], normalized: dict[str, object]) -> str:
layout_kind = str(source_layout.get("kind") or "unknown")
main_root = str(source_layout.get("main_configuration_root") or "не определена")
extension_roots = [str(item) for item in list(source_layout.get("extension_roots") or []) if str(item).strip()]
extensions_count = escape(str(normalized.get("extensions", len(extension_roots) or 0)))
return f"""
<div class="panel-title">Структура проекта для Codex</div>
<div class="ai-structure-hint-grid">
<article class="access-card">
<strong>Тип раскладки</strong>
<small>{escape(_layout_kind_text(layout_kind))}</small>
</article>
<article class="access-card">
<strong>Главная папка</strong>
<small>{escape(main_root)}</small>
</article>
<article class="access-card">
<strong>Расширений</strong>
<small>{extensions_count}</small>
</article>
<article class="access-card">
<strong>Первый вход для Codex</strong>
<small><code>context/project-brief.md</code>, <code>indexes/project-layout.json</code>, <code>indexes/objects-compact.json</code>, <code>indexes/modules-compact.json</code></small>
</article>
<article class="access-card">
<strong>Исходники</strong>
<small>В <code>source/</code> попадает выборочная UTF-8-копия BSL/MDO и ключевых XML, а не полный дубль выгрузки.</small>
</article>
<article class="access-card">
<strong>Папки расширений</strong>
<small>{escape(", ".join(extension_roots) or "нет")}</small>
</article>
</div>
"""
def _diagnostics(items: list[object]) -> str: def _diagnostics(items: list[object]) -> str:
if not items: if not items:
return "" return ""
@@ -267,6 +335,9 @@ def _artifact_text(value: object) -> str:
"ai_context.md": "Контекст для ИИ", "ai_context.md": "Контекст для ИИ",
"export_plan.md": "План выгрузки", "export_plan.md": "План выгрузки",
"codex_package": "Папка для Codex", "codex_package": "Папка для Codex",
"project_layout.json": "Карта раскладки проекта",
"compact_objects.json": "Компактный индекс объектов",
"compact_modules.json": "Компактный индекс модулей",
"sir_snapshot.json": "Снимок графа SIR", "sir_snapshot.json": "Снимок графа SIR",
"ai_objects.json": "Индекс объектов", "ai_objects.json": "Индекс объектов",
"ai_modules.json": "Индекс модулей", "ai_modules.json": "Индекс модулей",
@@ -291,3 +362,13 @@ def _agent_status_advice(status: str, agent_id: str) -> str:
if status == "offline": if status == "offline":
return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите." return f"Windows Agent {agent_id} выбран, но сейчас не отвечает. Обновите или запустите агент, затем повторите."
return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта." return "Для прямого разбора .cf/.cfe нужен выбранный Windows Agent. Укажите его в настройках проекта."
def _layout_kind_text(value: str) -> str:
mapping = {
"configuration_with_extensions": "Конфигурация + отдельные папки расширений",
"flat_or_mixed": "Плоская или смешанная выгрузка",
"file": "Отдельный входной файл",
"unknown": "Не определено",
}
return mapping.get(value, value)
@@ -14,12 +14,15 @@
.access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff} .access-builder{border-bottom:1px solid var(--line);background:#fff}.access-builder-form{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px;border-bottom:1px solid var(--line);background:#fbfcfe}.access-builder-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase}.access-builder-form input,.access-builder-form textarea{min-width:0;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.access-builder-form input{height:32px}.access-builder-form textarea{min-height:68px;padding:8px;resize:vertical}.access-builder-actions{grid-column:1/-1;display:flex;gap:8px;justify-content:flex-end}.access-builder-result{border-top:1px solid var(--line);background:#fff}
.access-card[hx-get]{cursor:pointer}.access-card[hx-get]:hover{background:#f8fbff}.access-user-detail{border-bottom:1px solid var(--line);background:#fff} .access-card[hx-get]{cursor:pointer}.access-card[hx-get]:hover{background:#f8fbff}.access-user-detail{border-bottom:1px solid var(--line);background:#fff}
.ai-structure-form{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe;overflow-x:hidden}.ai-structure-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase;min-width:0}.ai-structure-form input{height:32px;min-width:0;width:100%;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.ai-structure-field{min-width:0}.ai-structure-field-wide{grid-column:span 2}.ai-structure-field-compact{grid-column:span 1}.ai-structure-form .checkbox-row{display:flex;align-items:center;gap:7px;height:32px}.ai-structure-form .checkbox-row input{width:16px;height:16px;flex:0 0 auto}.ai-structure-submit{width:100%;margin:0;min-width:0}.ai-structure-result{background:#fff} .ai-structure-form{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;align-items:end;padding:12px 16px;border-bottom:1px solid var(--line);background:#fbfcfe;overflow-x:hidden}.ai-structure-form label{display:grid;gap:5px;font-size:11px;font-weight:900;color:var(--muted);text-transform:uppercase;min-width:0}.ai-structure-form input{height:32px;min-width:0;width:100%;border:1px solid var(--line);background:#fff;padding:0 8px;color:var(--text);font:13px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;text-transform:none}.ai-structure-field{min-width:0}.ai-structure-field-wide{grid-column:span 2}.ai-structure-field-compact{grid-column:span 1}.ai-structure-form .checkbox-row{display:flex;align-items:center;gap:7px;height:32px}.ai-structure-form .checkbox-row input{width:16px;height:16px;flex:0 0 auto}.ai-structure-submit{width:100%;margin:0;min-width:0}.ai-structure-result{background:#fff}
.ai-structure-hint{border-bottom:1px solid var(--line);background:#fff}.ai-structure-hint-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));background:#fff}.ai-structure-hint-grid .access-card:nth-child(3n+1),.ai-structure-hint-grid .access-card:nth-child(3n+2){border-right:1px solid var(--line)}.ai-structure-hint-grid code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}
.ai-structure-progress{border-bottom:1px solid var(--line);background:#fff}.ai-progress-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.ai-progress-head strong{flex:1}.ai-progress-head small{font-weight:900;color:var(--muted);font-variant-numeric:tabular-nums}.ai-progress-spinner{width:18px;height:18px;border:3px solid #c9d6ec;border-top-color:var(--brand);border-radius:50%;animation:aiProgressSpin 900ms linear infinite}.ai-progress-bar{height:6px;background:#eef2f7;overflow:hidden}.ai-progress-bar span{display:block;width:8%;height:100%;background:var(--brand);transition:width 700ms ease}.ai-progress-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin:0}.ai-progress-metrics div{padding:10px 12px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.ai-progress-metrics div:last-child{border-right:0}.ai-progress-metrics dt{font-size:11px;font-weight:900;text-transform:uppercase;color:var(--muted)}.ai-progress-metrics dd{margin:3px 0 0;font-weight:900}.ai-structure-progress[hidden]{display:none}.ai-structure-progress.htmx-request,.ai-structure-progress[data-ai-structure-progress-state="running"]{display:block}@keyframes aiProgressSpin{to{transform:rotate(360deg)}} .ai-structure-progress{border-bottom:1px solid var(--line);background:#fff}.ai-progress-head{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line);background:#fbfcfe}.ai-progress-head strong{flex:1}.ai-progress-head small{font-weight:900;color:var(--muted);font-variant-numeric:tabular-nums}.ai-progress-spinner{width:18px;height:18px;border:3px solid #c9d6ec;border-top-color:var(--brand);border-radius:50%;animation:aiProgressSpin 900ms linear infinite}.ai-progress-bar{height:6px;background:#eef2f7;overflow:hidden}.ai-progress-bar span{display:block;width:8%;height:100%;background:var(--brand);transition:width 700ms ease}.ai-progress-metrics{display:grid;grid-template-columns:repeat(3,1fr);margin:0}.ai-progress-metrics div{padding:10px 12px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.ai-progress-metrics div:last-child{border-right:0}.ai-progress-metrics dt{font-size:11px;font-weight:900;text-transform:uppercase;color:var(--muted)}.ai-progress-metrics dd{margin:3px 0 0;font-weight:900}.ai-structure-progress[hidden]{display:none}.ai-structure-progress.htmx-request,.ai-structure-progress[data-ai-structure-progress-state="running"]{display:block}@keyframes aiProgressSpin{to{transform:rotate(360deg)}}
@media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}} @media(max-width:980px){.layout{grid-template-columns:1fr;height:auto}.tree,.inspector{max-height:320px}.editor{min-height:520px}.hero{display:block}.shell{padding:16px}}
@media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}} @media(max-width:980px){.access-layout{grid-template-columns:1fr;height:auto}.access-nav,.access-side{max-height:360px}.access-role-grid{grid-template-columns:1fr}.access-role-grid .access-card:nth-child(odd){border-right:0}}
@media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}} @media(max-width:980px){.access-builder-form{grid-template-columns:1fr}.access-builder-actions{justify-content:stretch}.access-builder-actions button{flex:1}}
@media(max-width:1180px){.ai-structure-form{grid-template-columns:repeat(2,minmax(0,1fr))}.ai-structure-field-wide{grid-column:1/-1}} @media(max-width:1180px){.ai-structure-form{grid-template-columns:repeat(2,minmax(0,1fr))}.ai-structure-field-wide{grid-column:1/-1}}
@media(max-width:1180px){.ai-structure-hint-grid{grid-template-columns:1fr 1fr}.ai-structure-hint-grid .access-card:nth-child(3n+1),.ai-structure-hint-grid .access-card:nth-child(3n+2){border-right:0}.ai-structure-hint-grid .access-card:nth-child(odd){border-right:1px solid var(--line)}}
@media(max-width:700px){.ai-structure-form{grid-template-columns:1fr}.ai-structure-field-wide,.ai-structure-field-compact,.ai-structure-submit{grid-column:1/-1}} @media(max-width:700px){.ai-structure-form{grid-template-columns:1fr}.ai-structure-field-wide,.ai-structure-field-compact,.ai-structure-submit{grid-column:1/-1}}
@media(max-width:700px){.ai-structure-hint-grid{grid-template-columns:1fr}.ai-structure-hint-grid .access-card:nth-child(odd){border-right:0}}
@media(max-width:980px){.ai-progress-metrics{grid-template-columns:1fr}} @media(max-width:980px){.ai-progress-metrics{grid-template-columns:1fr}}
@media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}} @media(max-width:980px){.form-designer-body{grid-template-columns:1fr}.form-property-panel{border-left:0;border-top:1px solid var(--line)}.form-field[data-html5-form-width="half"],.form-field[data-html5-form-width="third"]{grid-column:1/-1}.form-editor-row[data-html5-form-element-editor]{grid-template-columns:1fr}.property-row{grid-template-columns:1fr}}
@media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}} @media(max-width:980px){.setup-layout{grid-template-columns:1fr}.setup-metrics{grid-template-columns:1fr 1fr}.settings-form{grid-template-columns:1fr}}
+3
View File
@@ -1767,6 +1767,9 @@ def test_ai_structure_prepare_writes_ai_ready_package(tmp_path: Path):
"Структура для ИИ", "Структура для ИИ",
"192.168.220.200", "192.168.220.200",
"Пути должны быть доступны серверу", "Пути должны быть доступны серверу",
"Ожидаемый формат выгрузки 1С",
"Конфигурация",
"Без лишнего сырья",
"smb_username", "smb_username",
"smb_password", "smb_password",
"data-ai-structure-progress", "data-ai-structure-progress",