Files
agentik-android/design/gen_mockups.py
T
subochev d5aa844fa0 design: макеты 15 экранов + принципы Android + откуда взят код
- design/mockups/ — 15 макетов (одно состояние = один файл), из /tmp перенесены в репо
- design/screenshots/ — те же экраны PNG
- design/PRINCIPLES.md — принципы: что берём у десктопа, что на телефоне иначе
- design/BORROW.md — что откуда: ядро (Nexus), тема (десктопный макет), отрисовка Markdown (ai/assistent)
- design/gen_mockups.py — генератор макетов, путь вывода теперь рядом с ним
- README: разделы «Принципы» и «Откуда код» со ссылками
2026-09-20 13:59:22 +03:00

864 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Генератор статичных макетов Android-клиента agentik (одно состояние = один файл).
Правило пользователя: интерактивность не нужна, вместо неё много html-файлов.
CSS и оболочка заданы ОДИН раз, состояние — фрагментом.
Телефон 390x844. Цвета в макете числами — так и надо; в коде приложения берутся
из цветовой схемы по смыслу (THEMES.md).
"""
import os
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mockups")
os.makedirs(OUT, exist_ok=True)
CSS = """
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2; --badge: #5EB5F7;
--err: #EC3942; --ok: #63C88A; --user-bubble: #2B5278; --bot-bubble: #1C2733;
--a1: #6AB2F2; --a2: #86C99A; --a3: #C9A886; --a4: #B79AE0; --a5: #E28C8C;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; flex-direction: column; align-items: center; justify-content: center;
min-height: 100vh; padding: 20px; gap: 12px;
}
.caption { font-size: 12.5px; color: var(--dim); letter-spacing: .2px; }
/* ── корпус телефона ── */
.phone {
width: 390px; height: 800px; flex: 0 0 auto; background: var(--bg);
border-radius: 38px; overflow: hidden; position: relative;
border: 1px solid #2A3340; display: flex; flex-direction: column;
box-shadow: 0 30px 90px rgba(0,0,0,.75);
}
.status { height: 38px; flex: 0 0 38px; display: flex; align-items: center;
justify-content: space-between; padding: 0 24px; font-size: 12px;
font-variant-numeric: tabular-nums; color: var(--fg); }
.status .r { display: flex; gap: 6px; align-items: center; font-size: 11px; }
/* ── шапки ── */
.appbar { height: 56px; flex: 0 0 56px; display: flex; align-items: center;
gap: 10px; padding: 0 12px; border-bottom: 1px solid var(--line);
background: var(--panel); }
.appbar.plain { background: var(--bg); }
.appbar .nm { font-size: 16px; font-weight: 600; }
.grow { flex: 1; min-width: 0; }
.ic { width: 40px; height: 40px; flex: 0 0 40px; border-radius: 12px; border: none;
background: transparent; color: var(--dim); font-size: 17px; cursor: pointer;
display: grid; place-items: center; font-family: inherit; }
.ic:hover { background: var(--panel2); color: var(--fg); }
.ava { width: 38px; height: 38px; flex: 0 0 38px; border-radius: 50%;
display: grid; place-items: center; font-size: 15px; font-weight: 700;
color: #0F1922; }
.hd { flex: 1; min-width: 0; }
.hd .t1 { font-size: 15px; font-weight: 600; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; }
.hd .t2 { font-size: 11.5px; color: var(--dim); }
.ok { color: var(--ok); }
.bad { color: var(--err); }
/* ── поиск и лента групп ── */
.search { margin: 10px 14px 8px; height: 40px; border-radius: 12px;
border: 1px solid var(--line); background: var(--panel); color: var(--dim);
font-size: 13.5px; display: flex; align-items: center; gap: 9px; padding: 0 13px; }
.chips { display: flex; gap: 7px; padding: 2px 14px 10px; align-items: center; }
.chip { padding: 7px 12px; border-radius: 16px; font-size: 12.5px; color: var(--dim);
background: var(--panel); white-space: nowrap; border: 1px solid transparent; }
.chip.on { color: var(--fg); border-color: var(--accent);
background: rgba(106,178,242,.13); }
.chip b { color: var(--badge); font-weight: 700; margin-left: 3px; }
.chip.act { margin-left: auto; background: transparent; padding: 7px 9px; }
/* ── список диалогов ── */
.list { flex: 1; overflow: hidden; padding: 2px 10px 12px; position: relative; }
.row { display: flex; align-items: center; gap: 11px; padding: 11px 10px;
border-radius: 13px; position: relative; }
.row.on { background: #233040; }
.row .strip { position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px;
border-radius: 3px; }
.row .rava { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 50%;
display: grid; place-items: center; font-size: 16px; font-weight: 700; color: #0F1922; }
.rmid { flex: 1; min-width: 0; }
.l1 { display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.l1 .n { font-size: 14.5px; font-weight: 500; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; }
.l1 .t { font-size: 11px; color: var(--dim); flex: 0 0 auto; }
.l2 { display: flex; justify-content: space-between; gap: 8px; align-items: center;
margin-top: 2px; }
.l2 .p { font-size: 12.5px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; }
.bg { background: var(--badge); color: #0F1922; border-radius: 11px; font-size: 11px;
font-weight: 700; padding: 1px 7px; flex: 0 0 auto; }
.fab { position: absolute; right: 20px; bottom: 22px; width: 56px; height: 56px;
border-radius: 18px; border: none; background: var(--accent); color: #0F1922;
font-size: 26px; font-weight: 600; display: grid; place-items: center;
box-shadow: 0 10px 26px rgba(106,178,242,.35); font-family: inherit; }
/* ── чат: сообщения ── */
.msgs { flex: 1; overflow: hidden; padding: 14px 14px 4px; display: flex;
flex-direction: column; gap: 10px; }
.msg { max-width: 84%; padding: 9px 13px; border-radius: 15px; font-size: 13.5px; }
.msg.user { align-self: flex-end; background: var(--user-bubble);
border-bottom-right-radius: 5px; }
.msg.bot { align-self: flex-start; background: var(--bot-bubble);
border-bottom-left-radius: 5px; }
.msg .meta { font-size: 10.5px; color: var(--dim); margin-top: 6px; }
.msg b { font-weight: 650; }
.msg code { background: rgba(255,255,255,.08); border-radius: 4px; padding: 1px 5px;
font-size: 12px; font-family: "DejaVu Sans Mono", monospace; }
.msg ul { margin: 6px 0 2px 16px; }
.msg li { margin-bottom: 3px; }
/* ход работы агента */
.act-line { align-self: stretch; display: flex; align-items: center; gap: 9px;
background: var(--panel); border: 1px solid var(--line); border-radius: 11px;
padding: 8px 12px; font-size: 12.5px; color: var(--dim); }
.act-line .g { color: var(--accent); font-size: 13px; }
.act-line .nm { color: var(--fg); }
.act-line .ch { margin-left: auto; font-size: 11px; }
.act-line.think { border-left: 2px solid var(--accent); border-radius: 0 11px 11px 0;
background: rgba(106,178,242,.07); }
.act-open { align-self: stretch; background: var(--panel); border: 1px solid var(--line);
border-radius: 11px; padding: 10px 12px; font-size: 12px; color: var(--dim); }
.act-open .h { display: flex; align-items: center; gap: 9px; color: var(--dim);
margin-bottom: 7px; }
.act-open .h .nm { color: var(--fg); }
.act-open .kv { display: flex; gap: 8px; margin-top: 4px; }
.act-open .kv .k { color: var(--dim); flex: 0 0 78px; }
.act-open .kv .v { color: var(--fg); }
.cur { display: inline-block; width: 7px; height: 14px; background: var(--accent);
vertical-align: -2px; margin-left: 2px; }
/* ── панель ввода ── */
.composer { border-top: 1px solid var(--line); background: var(--panel);
padding: 10px 12px 14px; }
.irow { display: flex; align-items: center; gap: 9px; }
.field { flex: 1; min-width: 0; height: 44px; border-radius: 14px; background: var(--bg);
border: 1px solid var(--line); display: flex; align-items: center; padding: 0 14px;
font-size: 13.5px; color: var(--dim); }
.mic { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 50%;
border: 1px solid var(--line); background: var(--bg); color: var(--dim);
font-size: 17px; display: grid; place-items: center; font-family: inherit; }
.mic.rec { background: var(--err); border-color: var(--err); color: #fff; }
.send { width: 44px; height: 44px; flex: 0 0 44px; border-radius: 14px; border: none;
background: var(--accent); color: #0F1922; font-size: 17px;
display: grid; place-items: center; font-family: inherit; }
.stop { height: 44px; flex: 1; border-radius: 14px; border: 1px solid rgba(236,57,66,.45);
background: rgba(236,57,66,.10); color: #F0A6AA; font-size: 13.5px; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 9px;
font-family: inherit; }
/* запись */
.recpanel { display: flex; align-items: center; gap: 11px; background: var(--bg);
border: 1px solid var(--err); border-radius: 14px; padding: 10px 13px;
margin-bottom: 10px; }
.wave { display: flex; align-items: center; gap: 3px; height: 22px; flex: 1; }
.wave i { width: 3px; background: var(--err); border-radius: 2px; }
.wave i:nth-child(1) { height: 30%; } .wave i:nth-child(2) { height: 70%; }
.wave i:nth-child(3) { height: 45%; } .wave i:nth-child(4) { height: 100%; }
.wave i:nth-child(5) { height: 55%; } .wave i:nth-child(6) { height: 80%; }
.wave i:nth-child(7) { height: 35%; } .wave i:nth-child(8) { height: 65%; }
.wave i:nth-child(9) { height: 25%; }
.rectime { font-variant-numeric: tabular-nums; font-size: 13.5px; color: var(--err);
font-weight: 700; }
.recnote { font-size: 11.5px; color: var(--dim); margin-top: 8px; text-align: center; }
/* ── модалки: нижний лист ── */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.6); }
.sheet { position: absolute; left: 0; right: 0; bottom: 0; background: var(--panel);
border-radius: 22px 22px 0 0; border-top: 1px solid var(--line);
max-height: 78%; display: flex; flex-direction: column; overflow: hidden; }
.sheet .grab { width: 40px; height: 4px; border-radius: 3px; background: var(--line);
margin: 10px auto 4px; flex: 0 0 auto; }
.sheet .hd2 { padding: 8px 18px 12px; }
.sheet .hd2 h2 { font-size: 17px; font-weight: 650; }
.sheet .hd2 p { font-size: 12.5px; color: var(--dim); margin-top: 4px; }
.sheet .body { flex: 1; overflow: hidden; padding: 0 14px 8px; }
.sheet .foot { padding: 12px 16px 18px; display: flex; gap: 10px;
border-top: 1px solid var(--line); }
/* ── карточка по центру ── */
.center { position: absolute; inset: 0; display: flex; align-items: center;
justify-content: center; padding: 22px; }
.card { width: 100%; max-height: 84%; background: var(--panel);
border: 1px solid var(--line); border-radius: 20px; display: flex;
flex-direction: column; overflow: hidden; }
/* ── кнопки ── */
.btn { border-radius: 13px; padding: 13px 16px; font-size: 14px; cursor: pointer;
border: 1px solid var(--line); background: transparent; color: var(--fg);
font-family: inherit; white-space: nowrap; }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922;
font-weight: 650; }
.btn.dang { color: var(--err); border-color: rgba(236,57,66,.4); }
.btn.dang.solid { background: var(--err); border-color: var(--err); color: #FFF;
font-weight: 650; }
.btn.off { background: var(--panel2); border-color: var(--panel2); color: var(--dim); }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center;
gap: 9px; border-style: dashed; color: var(--dim); padding: 15px; }
.btn.grow { flex: 1; }
.mini-ic { width: 38px; height: 38px; flex: 0 0 38px; border-radius: 11px; border: none;
background: transparent; color: var(--dim); font-size: 15px; display: grid;
place-items: center; font-family: inherit; }
.mini-ic:hover { background: var(--panel2); color: var(--fg); }
.mini-ic.del { color: #B4666B; }
/* ── строки-объекты (агенты, группы) ── */
.obj { display: flex; align-items: center; gap: 12px; padding: 13px 12px;
border-bottom: 1px solid var(--line); }
.obj .sign { width: 42px; height: 42px; flex: 0 0 42px; border-radius: 50%;
display: grid; place-items: center; font-size: 15px; font-weight: 700; color: #0F1922; }
.obj .mid { flex: 1; min-width: 0; }
.obj .nm { font-size: 14.5px; font-weight: 550; }
.obj .sb { font-size: 12px; color: var(--dim); white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; }
.obj .acts { display: flex; gap: 2px; flex: 0 0 auto; }
.obj .lock { font-size: 10.5px; color: var(--dim); border: 1px dashed var(--line);
border-radius: 8px; padding: 3px 8px; }
.sys .nm { color: var(--dim); }
/* ── формы ── */
.form { padding: 6px 16px 12px; overflow: hidden; }
.lbl { font-size: 12px; color: var(--dim); margin: 14px 0 6px; }
.inp { height: 46px; border-radius: 13px; background: var(--bg);
border: 1px solid var(--line); display: flex; align-items: center; padding: 0 14px;
font-size: 13.5px; }
.inp.ph { color: var(--dim); }
.inp.focus { border-color: var(--accent); }
.inp .tail { margin-left: auto; font-size: 12px; color: var(--accent); }
.note { font-size: 11.5px; color: var(--dim); margin-top: 6px; line-height: 1.5; }
.tabs { display: flex; gap: 8px; margin: 14px 0 12px; }
.tab { flex: 1; text-align: center; padding: 11px; border-radius: 12px;
font-size: 13.5px; color: var(--dim); background: var(--panel2); }
.tab.on { color: var(--fg); background: rgba(106,178,242,.14);
border: 1px solid var(--accent); padding: 10px; }
.swatches { display: flex; gap: 13px; padding: 4px 2px; }
.sw { width: 34px; height: 34px; border-radius: 50%; border: 2px solid transparent; }
.sw.on { border-color: var(--fg); }
/* ── проверка связи ── */
.chk { display: flex; flex-direction: column; align-items: center; text-align: center;
padding: 24px 18px 18px; }
.ring { width: 62px; height: 62px; border-radius: 50%; border: 3px solid var(--panel2);
border-top-color: var(--accent); margin-bottom: 14px; }
.tick { width: 62px; height: 62px; border-radius: 50%; background: rgba(99,200,138,.14);
border: 2px solid var(--ok); color: var(--ok); font-size: 28px; display: grid;
place-items: center; margin-bottom: 14px; }
.cross { width: 62px; height: 62px; border-radius: 50%; background: rgba(236,57,66,.12);
border: 2px solid var(--err); color: var(--err); font-size: 26px; display: grid;
place-items: center; margin-bottom: 14px; }
.chk .st { font-size: 16px; font-weight: 600; }
.chk .ds { font-size: 12.5px; color: var(--dim); margin-top: 5px; }
.kv2 { width: 100%; margin-top: 16px; border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line); padding: 10px 0; }
.kv2 .r { display: flex; justify-content: space-between; gap: 10px; font-size: 12.5px;
padding: 4px 0; }
.kv2 .r .k { color: var(--dim); }
.kv2 .r .v { color: var(--fg); text-align: right; }
.kv2 .r .v.ok { color: var(--ok); } .kv2 .r .v.bad { color: var(--err); }
/* ── предупреждение ── */
.warn { display: flex; gap: 10px; align-items: flex-start; padding: 12px 13px;
border-radius: 12px; background: rgba(236,57,66,.09);
border: 1px solid rgba(236,57,66,.32); font-size: 12px; color: #F0A6AA;
line-height: 1.5; }
.warn b { color: #FFC9CB; }
.danger-blk { margin-top: 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.danger-blk .cap { font-size: 11px; letter-spacing: .8px; color: var(--err);
font-weight: 700; margin-bottom: 8px; }
/* ── пустое состояние ── */
.empty { flex: 1; display: flex; flex-direction: column; align-items: center;
justify-content: center; text-align: center; padding: 30px 34px; gap: 12px; }
.empty .big { width: 74px; height: 74px; border-radius: 22px; background: var(--panel);
border: 1px solid var(--line); display: grid; place-items: center; font-size: 30px;
color: var(--dim); }
.empty h3 { font-size: 16.5px; font-weight: 600; }
.empty p { font-size: 13px; color: var(--dim); line-height: 1.55; }
/* ── радио в выборе ассистента ── */
.radio { width: 22px; height: 22px; flex: 0 0 22px; border-radius: 50%;
border: 2px solid var(--line); }
.radio.on { border-color: var(--accent); background: radial-gradient(circle,
var(--accent) 0 6px, transparent 7px); }
"""
def statusbar():
return ('<div class="status"><span>9:41</span>'
'<span class="r">▮▮▮ ⏸ 82%</span></div>')
def chips(active="Все чаты"):
"""Лента групп над списком. Карандаш — управление группами, прижат вправо."""
items = [("Все чаты", "3"), ("Работа", ""), ("Дом", "")]
out = []
for n, c in items:
cls = " on" if n == active else ""
cnt = f' <b>{c}</b>' if c else ""
out.append(f'<span class="chip{cls}">{n}{cnt}</span>')
out.append('<span class="chip act">✎</span>')
return '<div class="chips">' + "".join(out) + "</div>"
def row(name, prev, time, color, letter, badge="", active=False):
cls = " on" if active else ""
b = f'<span class="bg">{badge}</span>' if badge else ""
return f'''<div class="row{cls}">
<div class="strip" style="background:{color}"></div>
<div class="rava" style="background:{color}">{letter}</div>
<div class="rmid">
<div class="l1"><span class="n">{name}</span><span class="t">{time}</span></div>
<div class="l2"><span class="p">{prev}</span>{b}</div>
</div>
</div>'''
# ─────────────────────────────────────────────────────────────
# СОСТОЯНИЯ
# ─────────────────────────────────────────────────────────────
CHATS = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<div class="nm">Диалоги</div>
<div class="grow"></div>
<button class="ic">⚙</button>
</div>
<div class="search"><span>⌕</span>Поиск по диалогам</div>
{chips("Все чаты")}
<div class="list">
{row("Ремонт на кухне", "Плитку лучше матовую — бликов не будет", "14:32", "var(--a1)", "С", "3", True)}
{row("Отчёт за квартал", "Готово. Таблицу собрал, отправил в файл", "13:05", "var(--a2)", "А")}
{row("Ремонт на кухне", "Смеситель взял в расчёт, 12 400 ₽", "Вчера", "var(--a1)", "С")}
{row("Список покупок", "Молоко, хлеб, кофе — записал", "Вчера", "var(--a3)", "Д")}
{row("Отчёт за квартал", "Уточни, за какой период считать", "12.09", "var(--a2)", "А", "1")}
{row("Разбор документации", "Нашёл три противоречия в разделах", "11.09", "var(--a4)", "У")}
</div>
<button class="fab">+</button>
</div>'''
CHAT = f'''<div class="phone">
{statusbar()}
<div class="appbar chat">
<button class="ic">←</button>
<div class="ava" style="background:var(--a1)">С</div>
<div class="hd">
<div class="t1">Ремонт на кухне</div>
<div class="t2">Строитель · <span class="ok">на связи</span></div>
</div>
<button class="ic">⋮</button>
</div>
<div class="msgs">
<div class="msg user">Чем закончить стык плитки и столешницы?</div>
<div class="act-line think"><span class="g">✦</span>
<span>рассуждаю: нужен герметик или профиль</span>
<span class="ch">▾</span></div>
<div class="act-line"><span class="g">⚙</span>
<span>вызов инструмента: <span class="nm">web_search</span></span>
<span class="ch">▾</span></div>
<div class="msg bot">Проверил варианты. Практичнее всего <b>силиконовый профиль</b>:
<ul><li>не боится воды и пара;</li><li>снимается без следа, если понадобится переклеить.</li></ul>
Герметик дешевле примерно на <code>15%</code>, но через год темнеет.
<div class="meta">14:32</div></div>
<div class="msg user">А если столешница из массива?</div>
<div class="act-open">
<div class="h"><span class="g">⚙</span><span class="nm">вызов инструмента: web_search</span>
<span style="margin-left:auto">▴</span></div>
<div class="kv"><span class="k">запрос</span><span class="v">силиконовый профиль массив дуба</span></div>
<div class="kv"><span class="k">результат</span><span class="v">8 источников, 0,4 с</span></div>
</div>
<div class="msg bot">Тогда профиль лучше не ставить — массив «дышит». Возьмите
герметик для дерева.
<div class="meta">14:33</div></div>
</div>
<div class="composer">
<div class="irow">
<div class="field">Сообщение…</div>
<button class="mic">🎤</button>
<button class="send">➤</button>
</div>
</div>
</div>'''
STREAMING = f'''<div class="phone">
{statusbar()}
<div class="appbar chat">
<button class="ic">←</button>
<div class="ava" style="background:var(--a1)">С</div>
<div class="hd">
<div class="t1">Ремонт на кухне</div>
<div class="t2">Строитель · <span class="ok">на связи</span></div>
</div>
<button class="ic">⋮</button>
</div>
<div class="msgs">
<div class="msg user">А если столешница из массива?</div>
<div class="act-line"><span class="g">⚙</span>
<span>вызов инструмента: <span class="nm">web_search</span></span>
<span class="ch">▾</span></div>
<div class="msg bot">Тогда профиль лучше не ставить — массив «дышит»,
а жёсткая планка не даст ему двигаться. Возьмите герметик для дерева:
он остаётся эластичным<span class="cur"></span>
<div class="meta">14:33</div></div>
</div>
<div class="composer">
<div class="irow">
<button class="stop">■ Стоп</button>
</div>
<div class="recnote">Агент отвечает — можно прервать</div>
</div>
</div>'''
RECORDING = f'''<div class="phone">
{statusbar()}
<div class="appbar chat">
<button class="ic">←</button>
<div class="ava" style="background:var(--a1)">С</div>
<div class="hd">
<div class="t1">Ремонт на кухне</div>
<div class="t2">Строитель · <span class="ok">на связи</span></div>
</div>
<button class="ic">⋮</button>
</div>
<div class="msgs">
<div class="msg bot">Тогда профиль лучше не ставить — массив «дышит».
<div class="meta">14:33</div></div>
<div class="msg user">Скинь, что брать по смесителю</div>
<div class="msg bot">Смеситель с выдвижным изливом, <code>GROHE</code> или
<code>Hansgrohe</code>.<div class="meta">14:34</div></div>
</div>
<div class="composer">
<div class="recpanel">
<div class="wave"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>
<div class="rectime">0:07</div>
<button class="mini-ic del" style="width:34px;height:34px;flex:0 0 34px">✕</button>
<button class="send" style="width:38px;height:38px;flex:0 0 38px">➤</button>
</div>
<div class="irow">
<div class="field">Идёт запись…</div>
<button class="mic rec">■</button>
</div>
<div class="recnote">Говорите — текст появится в поле. ✕ отменяет запись целиком.</div>
</div>
</div>'''
EMPTY = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<div class="nm">Диалоги</div>
<div class="grow"></div>
<button class="ic">⚙</button>
</div>
<div class="empty">
<div class="big">◇</div>
<h3>Пока не добавлен ни один ассистент</h3>
<p>Диалог живёт внутри ассистента — создать его отдельно нельзя.
Сначала добавьте ассистента: укажите название и адрес сервера.</p>
<button class="btn prim" style="margin-top:6px">Добавить ассистента</button>
</div>
</div>'''
AGENTS = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Ассистенты</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:6px 8px">
<div class="obj">
<div class="sign" style="background:var(--a1)">С</div>
<div class="mid"><div class="nm">Строитель</div>
<div class="sb">http://192.168.88.21:8080/agentik</div>
<div class="sb" style="margin-top:2px"><span class="ok">● на связи</span></div></div>
<div class="acts"><button class="mini-ic">⇄</button><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div class="obj">
<div class="sign" style="background:var(--a2)">А</div>
<div class="mid"><div class="nm">Аналитик</div>
<div class="sb">http://192.168.88.19:8080/agentik</div>
<div class="sb" style="margin-top:2px"><span class="bad">● не отвечает</span></div></div>
<div class="acts"><button class="mini-ic">⇄</button><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div class="obj">
<div class="sign" style="background:var(--a3)">Д</div>
<div class="mid"><div class="nm">Домовой</div>
<div class="sb">http://192.168.76.153:8080/agentik</div>
<div class="sb" style="margin-top:2px;color:var(--dim)">● не проверялся</div></div>
<div class="acts"><button class="mini-ic">⇄</button><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div style="padding:14px 4px"><button class="btn wide">+ Добавить ассистента</button></div>
</div>
</div>'''
FORM_COLOR = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Новый ассистент</div>
<div class="grow"></div>
</div>
<div class="form" style="flex:1">
<div class="lbl">Название</div>
<div class="inp">Строитель</div>
<div class="note">Имя придумываете вы. Сервер своего имени не сообщает.</div>
<div class="lbl">Адрес сервера</div>
<div class="inp focus">http://192.168.88.21:8080/agentik<span class="tail">⇄</span></div>
<div class="note">Адрес целиком, вместе с путём <code style="color:var(--dim)">/agentik</code>.
Значок ⇄ проверяет связь.</div>
<div class="lbl">Как отличать в списке</div>
<div class="tabs">
<div class="tab on">Цвет</div>
<div class="tab">Картинка</div>
</div>
<div class="swatches">
<div class="sw on" style="background:var(--a1)"></div>
<div class="sw" style="background:var(--a2)"></div>
<div class="sw" style="background:var(--a3)"></div>
<div class="sw" style="background:var(--a4)"></div>
<div class="sw" style="background:var(--a5)"></div>
</div>
<div class="note">Этим цветом помечаются диалоги ассистента в списке.</div>
</div>
<div class="composer" style="display:flex;gap:10px">
<button class="btn grow">Отмена</button>
<button class="btn prim grow">Сохранить</button>
</div>
</div>'''
FORM_PIC = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Новый ассистент</div>
<div class="grow"></div>
</div>
<div class="form" style="flex:1">
<div class="lbl">Название</div>
<div class="inp">Строитель</div>
<div class="lbl">Адрес сервера</div>
<div class="inp">http://192.168.88.21:8080/agentik<span class="tail">⇄</span></div>
<div class="lbl">Как отличать в списке</div>
<div class="tabs">
<div class="tab">Цвет</div>
<div class="tab on">Картинка</div>
</div>
<div style="display:flex;align-items:center;gap:14px;padding:4px 0">
<div style="width:62px;height:62px;flex:0 0 62px;border-radius:50%;
background:var(--a2);display:grid;place-items:center;font-size:24px;
color:#0F1922">🏠</div>
<button class="btn grow">Выбрать файл…</button>
<button class="btn dang">Убрать</button>
</div>
<div class="note">Картинка <b style="color:var(--fg)">копируется внутрь клиента</b> —
по исходному адресу не берётся. Файл с флешки могут убрать, и значок пропадёт.</div>
</div>
<div class="composer" style="display:flex;gap:10px">
<button class="btn grow">Отмена</button>
<button class="btn prim grow">Сохранить</button>
</div>
</div>'''
CHECK_OK = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Ассистенты</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:6px 8px;opacity:.35">
<div class="obj"><div class="sign" style="background:var(--a1)">С</div>
<div class="mid"><div class="nm">Строитель</div>
<div class="sb">http://192.168.88.21:8080/agentik</div></div></div>
<div class="obj"><div class="sign" style="background:var(--a2)">А</div>
<div class="mid"><div class="nm">Аналитик</div>
<div class="sb">http://192.168.88.19:8080/agentik</div></div></div>
</div>
<div class="scrim"></div>
<div class="center">
<div class="card">
<div class="chk">
<div class="tick">✓</div>
<div class="st">Ассистент отвечает</div>
<div class="ds">Можно сохранять</div>
<div class="kv2">
<div class="r"><span class="k">Ответ на запрос</span><span class="v ok">ok</span></div>
<div class="r"><span class="k">Адрес</span>
<span class="v">192.168.88.21:8080</span></div>
<div class="r"><span class="k">Диалогов</span><span class="v">7</span></div>
<div class="r"><span class="k">Отклик</span><span class="v">34 мс</span></div>
</div>
</div>
<div class="foot">
<button class="btn grow">Закрыть</button>
<button class="btn prim grow">Добавить</button>
</div>
</div>
</div>
</div>'''
CHECK_WAIT = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Ассистенты</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:6px 8px;opacity:.35">
<div class="obj"><div class="sign" style="background:var(--a1)">С</div>
<div class="mid"><div class="nm">Строитель</div>
<div class="sb">http://192.168.88.21:8080/agentik</div></div></div>
</div>
<div class="scrim"></div>
<div class="center">
<div class="card">
<div class="chk">
<div class="ring"></div>
<div class="st">Спрашиваю ассистента…</div>
<div class="ds">Отправлен запрос состояния и запрос списка диалогов.
Оба ничего не меняют.</div>
</div>
<div class="foot">
<button class="btn grow off">Добавить</button>
</div>
</div>
</div>
</div>'''
CHECK_FAIL = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Ассистенты</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:6px 8px;opacity:.35">
<div class="obj"><div class="sign" style="background:var(--a2)">А</div>
<div class="mid"><div class="nm">Аналитик</div>
<div class="sb">http://192.168.88.19:8080/agentik</div></div></div>
</div>
<div class="scrim"></div>
<div class="center">
<div class="card">
<div class="chk">
<div class="cross">✕</div>
<div class="st">Ассистент не отвечает</div>
<div class="ds">Проверьте адрес и что сервер запущен</div>
<div class="kv2">
<div class="r"><span class="k">Ответ на запрос</span>
<span class="v bad">соединение не установлено</span></div>
<div class="r"><span class="k">Адрес</span><span class="v">192.168.88.19:8080</span></div>
<div class="r"><span class="k">Отклик</span><span class="v">5 000 мс</span></div>
</div>
</div>
<div class="foot">
<button class="btn grow">Закрыть</button>
<button class="btn grow">Проверить снова</button>
</div>
</div>
</div>
</div>'''
NEW_CHAT = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<div class="nm">Диалоги</div>
<div class="grow"></div>
<button class="ic">⚙</button>
</div>
<div class="search"><span>⌕</span>Поиск по диалогам</div>
{chips("Все чаты")}
<div class="list" style="opacity:.4">
{row("Ремонт на кухне", "Плитку лучше матовую", "14:32", "var(--a1)", "С", "3")}
{row("Отчёт за квартал", "Готово. Таблицу собрал", "13:05", "var(--a2)", "А")}
{row("Список покупок", "Молоко, хлеб, кофе", "Вчера", "var(--a3)", "Д")}
</div>
<div class="scrim"></div>
<div class="sheet">
<div class="grab"></div>
<div class="hd2"><h2>Новый диалог</h2>
<p>В каком ассистенте создать? Диалог живёт внутри ассистента.</p></div>
<div class="body">
<div class="obj" style="border:none;border-radius:12px;background:rgba(106,178,242,.08)">
<div class="sign" style="background:var(--a1)">С</div>
<div class="mid"><div class="nm">Строитель</div>
<div class="sb">http://192.168.88.21:8080/agentik</div>
<div class="sb" style="margin-top:2px"><span class="ok">● на связи</span></div></div>
<div class="radio on"></div>
</div>
<div class="obj" style="border:none">
<div class="sign" style="background:var(--a2)">А</div>
<div class="mid"><div class="nm">Аналитик</div>
<div class="sb">http://192.168.88.19:8080/agentik</div>
<div class="sb" style="margin-top:2px"><span class="bad">● не отвечает</span></div></div>
<div class="radio"></div>
</div>
</div>
<div class="foot">
<button class="btn grow">Отмена</button>
<button class="btn prim grow">Создать</button>
</div>
</div>
</div>'''
GROUPS = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Группы</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:8px 10px">
<div class="obj sys">
<div class="mid"><div class="nm">Все чаты</div>
<div class="sb">Показывает все диалоги, из любой группы</div></div>
<div class="lock">встроенная</div>
</div>
<div class="obj">
<div class="mid"><div class="nm">Работа</div><div class="sb">12 диалогов</div></div>
<div class="acts"><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div class="obj">
<div class="mid"><div class="nm">Дом</div><div class="sb">4 диалога</div></div>
<div class="acts"><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div class="obj">
<div class="mid"><div class="nm">Учёба</div><div class="sb">2 диалога</div></div>
<div class="acts"><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div class="obj" style="border:none">
<div class="mid"><div class="nm">Пустая</div>
<div class="sb">Ни одного диалога</div></div>
<div class="acts"><button class="mini-ic">✎</button>
<button class="mini-ic del">⌫</button></div>
</div>
<div style="padding:14px 2px"><button class="btn wide">+ Добавить группу</button></div>
</div>
</div>'''
GROUP_EDIT = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Группы</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:8px 10px;opacity:.3">
<div class="obj sys"><div class="mid"><div class="nm">Все чаты</div></div></div>
<div class="obj"><div class="mid"><div class="nm">Работа</div>
<div class="sb">12 диалогов</div></div></div>
<div class="obj"><div class="mid"><div class="nm">Дом</div>
<div class="sb">4 диалога</div></div></div>
</div>
<div class="scrim"></div>
<div class="center">
<div class="card">
<div style="padding:18px 18px 4px"><div class="nm" style="font-size:17px;
font-weight:650">Изменить группу</div></div>
<div class="form" style="padding:6px 18px 14px">
<div class="lbl">Название</div>
<div class="inp focus">Работа</div>
<div class="danger-blk">
<div class="cap">ОПАСНОЕ</div>
<button class="btn dang" style="width:100%">Удалить группу</button>
<div class="note">Диалоги останутся — они вернутся в «Все чаты».</div>
</div>
</div>
<div class="foot">
<button class="btn grow">Отмена</button>
<button class="btn prim grow">Сохранить</button>
</div>
</div>
</div>
</div>'''
GROUP_DELETE = f'''<div class="phone">
{statusbar()}
<div class="appbar">
<button class="ic">←</button>
<div class="nm">Группы</div>
<div class="grow"></div>
</div>
<div style="flex:1;overflow:hidden;padding:8px 10px;opacity:.3">
<div class="obj sys"><div class="mid"><div class="nm">Все чаты</div></div></div>
<div class="obj"><div class="mid"><div class="nm">Работа</div>
<div class="sb">12 диалогов</div></div></div>
</div>
<div class="scrim"></div>
<div class="center">
<div class="card">
<div style="padding:18px 18px 4px"><div class="nm" style="font-size:17px;
font-weight:650">Удалить группу «Работа»?</div></div>
<div class="form" style="padding:12px 18px 16px">
<div class="warn"><span>⚠</span><span><b>Диалоги не удаляются.</b>
12 диалогов останутся и будут видны в «Все чаты». Сама группа исчезнет,
и диалоги потеряют к ней привязку.</span></div>
</div>
<div class="foot">
<button class="btn grow">Отмена</button>
<button class="btn dang solid grow">Удалить</button>
</div>
</div>
</div>
</div>'''
FILES = {
"01-chats.html": ("Диалоги — список", CHATS),
"02-chat.html": ("Чат — открытый диалог", CHAT),
"03-streaming.html": ("Чат — ответ идёт, кнопка «Стоп»", STREAMING),
"04-recording.html": ("Чат — запись голоса", RECORDING),
"05-empty.html": ("Диалоги — ни одного ассистента", EMPTY),
"06-agents.html": ("Ассистенты — список", AGENTS),
"07-agent-color.html": ("Ассистент — форма, цвет", FORM_COLOR),
"08-agent-pic.html": ("Ассистент — форма, картинка", FORM_PIC),
"09-check-wait.html": ("Проверка связи — идёт", CHECK_WAIT),
"10-check-ok.html": ("Проверка связи — отвечает", CHECK_OK),
"11-check-fail.html": ("Проверка связи — не отвечает", CHECK_FAIL),
"12-new-chat.html": ("Новый диалог — выбор ассистента", NEW_CHAT),
"13-groups.html": ("Группы — список", GROUPS),
"14-group-edit.html": ("Группы — изменение", GROUP_EDIT),
"15-group-delete.html": ("Группы — удаление", GROUP_DELETE),
}
PAGE = '''<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>{title}</title>
<style>{css}</style>
</head>
<body>
<div class="caption">{title}</div>
{body}
</body>
</html>
'''
if __name__ == "__main__":
for name, (title, body) in FILES.items():
html = PAGE.format(title=title, css=CSS, body=body)
path = os.path.join(OUT, name)
with open(path, "w", encoding="utf-8") as f:
f.write(html)
print(f"{name:22} {os.path.getsize(path):>7} байт")
print(f"\n→ {OUT} ({len(FILES)} состояний)")