t-261009160906: анти-гонка уровней min/hour на входящих TG — привязка артефакта к сообщению (-m), guard в deliver_artifact, reply_artifact в read model, тест гонки (8 PASS) + концепт ИИШНИЦЫ
This commit is contained in:
parent
37fe77f923
commit
58cc3785c6
27 changed files with 2912 additions and 22 deletions
|
|
@ -40,6 +40,8 @@
|
|||
.hero h1{font-size:clamp(30px,4.5vw,46px);line-height:1.1;letter-spacing:-.02em;max-width:16ch}
|
||||
.hero p{margin-top:14px;max-width:48ch;color:var(--muted);font-size:17px}
|
||||
.hero .btn{display:inline-block;margin-top:22px;background:var(--acc);color:#fff;padding:13px 26px;border-radius:6px;font-weight:600}
|
||||
.hero .btn.ghost{background:transparent;color:var(--acc);border:1.5px solid var(--acc);margin-left:10px}
|
||||
.hero .btn.ghost:hover{background:var(--acc-soft)}
|
||||
.hero .btn:hover{filter:brightness(1.08)}
|
||||
.rhythm{flex:1;min-width:260px;display:flex;align-items:flex-end;gap:4px;height:120px;opacity:.9}
|
||||
.rhythm span{flex:1;background:var(--acc);border-radius:3px 3px 0 0;animation:grow 2.8s ease-in-out infinite}
|
||||
|
|
@ -94,9 +96,25 @@
|
|||
.dark input:focus{border-color:var(--acc)}
|
||||
.dark button{background:var(--acc);border:none;color:#fff;padding:12px 22px;border-radius:6px;font:inherit;font-weight:600;cursor:pointer}
|
||||
|
||||
.plan-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:8px}
|
||||
.plan-card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:24px}
|
||||
.plan-card h3{font-size:20px}
|
||||
.plan-price{font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:6px 0 14px}
|
||||
.plan-card ul{list-style:none;margin:0 0 18px}
|
||||
.plan-card li{padding:6px 0 6px 22px;position:relative;font-size:14.5px;color:#3d3a34}
|
||||
.plan-card li:before{content:"✓";position:absolute;left:0;color:var(--acc);font-weight:700}
|
||||
.plan-card .btn{display:inline-block;background:var(--acc);color:#fff;padding:10px 18px;border-radius:6px;font-weight:600}
|
||||
.plan-card.featured{border:2px solid var(--acc);box-shadow:0 6px 24px rgba(14,122,95,.12)}
|
||||
.plan-card code{font-family:var(--mono);font-size:12px;background:var(--acc-soft);color:var(--acc);padding:1px 5px;border-radius:4px}
|
||||
#reqform{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;max-width:760px}
|
||||
#reqform button{margin-top:4px}
|
||||
|
||||
footer{padding:30px 0 40px;color:var(--muted);font-size:13.5px}
|
||||
footer .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}
|
||||
footer .proto{font-family:var(--mono);font-size:12px}
|
||||
footer .flinks{display:flex;gap:18px;font-size:13.5px}
|
||||
footer .flinks a{text-decoration:underline;color:var(--muted)}
|
||||
footer .flinks a:hover{color:var(--acc)}
|
||||
|
||||
#toast{position:fixed;left:50%;bottom:24px;transform:translate(-50%,80px);background:var(--ink);color:#fff;padding:12px 20px;border-radius:8px;font-size:14px;opacity:0;transition:.25s ease;z-index:9}
|
||||
#toast.show{transform:translate(-50%,0);opacity:1}
|
||||
|
|
@ -113,7 +131,7 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<div class="note">Прототип витрины — шаг 1 из плана Shkor beauty. Наполнение демонстрационное, оплата не подключена.</div>
|
||||
<div class="note">Витрина работает: AI-ассистент подбирает уход, заявки принимаются (Lite-план). Оплата онлайн — на шаге 2.</div>
|
||||
|
||||
<header>
|
||||
<div class="wrap">
|
||||
|
|
@ -126,6 +144,7 @@
|
|||
<div class="hlinks">
|
||||
<a href="#catalog">Каталог</a>
|
||||
<a href="#brands">Бренды</a>
|
||||
<a href="#plans">Магазинам</a>
|
||||
<a href="#sub">Подписка</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -143,9 +162,9 @@
|
|||
<div class="hero">
|
||||
<div class="wrap">
|
||||
<div>
|
||||
<h1>Корейский уход — с полки до двери за 1 день</h1>
|
||||
<p>Каталог проверенных K-beauty брендов в Минске: очищение, тонизирование, сыворотки, SPF. Самовывоз и доставка по Беларуси.</p>
|
||||
<a class="btn" href="#catalog">Смотреть каталог</a>
|
||||
<h1>Корейский уход — AI подберёт, привезём за 1 день</h1>
|
||||
<p>Каталог проверенных K-beauty брендов в Минске + AI-ассистент: разберёт состояние кожи, соберёт рутину и примет заявку. Доставка по Беларуси.</p>
|
||||
<a class="btn" href="#catalog">Смотреть каталог</a> <a class="btn ghost" href="#request">Оставить заявку</a>
|
||||
</div>
|
||||
<div class="rhythm" aria-hidden="true">
|
||||
<span></span><span></span><span></span><span></span><span></span><span></span>
|
||||
|
|
@ -321,6 +340,51 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section id="plans">
|
||||
<div class="wrap">
|
||||
<h2>Магазинам корейской косметики</h2>
|
||||
<p class="sub">Shkor beauty — это AI-ассистент витрины (B2B): подбор ухода, заказы и заявки — единое API для вашего сайта и любых агентов. Оператор сервиса — Shkor Labs LLC.</p>
|
||||
<div class="plan-grid">
|
||||
<div class="plan-card">
|
||||
<h3>Lite</h3>
|
||||
<p class="plan-price">базовый план</p>
|
||||
<ul>
|
||||
<li>Витрина с каталогом и брендами</li>
|
||||
<li>Форма заявки → общая база заказов</li>
|
||||
<li>Заявки приходят с пометкой <code>plan=lite</code></li>
|
||||
<li>Договор с магазином лично: телефон/мессенджер</li>
|
||||
</ul>
|
||||
<a class="btn" href="#request">Начать с заявки</a>
|
||||
</div>
|
||||
<div class="plan-card featured">
|
||||
<h3>Full</h3>
|
||||
<p class="plan-price">полный каталог + заказы</p>
|
||||
<ul>
|
||||
<li>Всё из Lite</li>
|
||||
<li>Полный каталог и корзина</li>
|
||||
<li>Онлайн-оплата и статусы доставки</li>
|
||||
<li>Агентный API: analyze → recommend → order</li>
|
||||
</ul>
|
||||
<a class="btn" href="#request">Подключить Full</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dark" id="request">
|
||||
<div class="wrap">
|
||||
<h2>Оставить заявку</h2>
|
||||
<p class="sub">Расскажите, что нужно — подберём уход или подключим магазин. Заявка пишется в общую базу заказов, ответим в течение дня.</p>
|
||||
<form id="reqform">
|
||||
<input type="text" name="name" placeholder="Имя" required>
|
||||
<input type="tel" name="phone" placeholder="+375 XX XXX-XX-XX" required>
|
||||
<input type="text" name="note" placeholder="Что подобрать? (сухая кожа, SPF, тонер…)">
|
||||
<button type="submit">Отправить</button>
|
||||
</form>
|
||||
<p class="sub" id="reqstatus" style="margin-top:10px"></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dark" id="sub">
|
||||
<div class="wrap">
|
||||
<h2>Подписка на поступления</h2>
|
||||
|
|
@ -334,7 +398,13 @@
|
|||
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<div>Shkor beauty — витрина корейской косметики. Прототип шага 1: каталог без платежей.</div>
|
||||
<div>Shkor beauty — витрина корейской косметики + AI-ассистент. Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com" style="text-decoration:underline">alex.shkor@gmail.com</a></div>
|
||||
<div class="flinks">
|
||||
<a href="/privacy.html">Privacy Policy</a>
|
||||
<a href="/terms.html">Terms of Service</a>
|
||||
<a href="/disclaimer.html">Disclaimer</a>
|
||||
<a href="/about.html">About & Contacts</a>
|
||||
</div>
|
||||
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -347,7 +417,25 @@ function toast(t){
|
|||
clearTimeout(el._t);el._t=setTimeout(function(){el.classList.remove('show')},2200);
|
||||
}
|
||||
document.querySelectorAll('.p button').forEach(function(b){
|
||||
b.addEventListener('click',function(){toast('Прототип: заказ и оплата будут на шаге 2')});
|
||||
b.addEventListener('click',function(){
|
||||
var card=b.closest('.pb'), h=card?card.querySelector('h3'):null;
|
||||
var note=document.getElementById('reqform').note;
|
||||
if(h&¬e) note.value='Интересует: '+h.textContent;
|
||||
document.getElementById('request').scrollIntoView({behavior:'smooth'});
|
||||
toast('Добавлено в заявку — оставьте контакты ниже');
|
||||
});
|
||||
});
|
||||
document.getElementById('reqform').addEventListener('submit',function(e){
|
||||
e.preventDefault();
|
||||
var f=e.target, st=document.getElementById('reqstatus');
|
||||
st.textContent='Отправляем…';
|
||||
fetch('/api/call',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({name:'submit_request',arguments:{name:f.name.value.trim(),phone:f.phone.value.trim(),note:f.note.value.trim()}})
|
||||
}).then(function(r){return r.json()}).then(function(d){
|
||||
var res=d.result||d;
|
||||
if(res.order_id){st.textContent='Заявка принята: '+res.order_id+'. Свяжемся в течение дня.';f.reset();}
|
||||
else{st.textContent='Не получилось: '+(res.error||JSON.stringify(res));}
|
||||
}).catch(function(err){st.textContent='Сеть недоступна — напишите на alex.shkor@gmail.com';});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -255,6 +255,8 @@ def place_order(args: dict) -> dict:
|
|||
|
||||
oid = "SB-" + uuid.uuid4().hex[:8].upper()
|
||||
order = {
|
||||
"order_id": oid,
|
||||
"plan": args.get("plan") if args.get("plan") in ("full", "lite") else "full",
|
||||
"order_id": oid,
|
||||
"items": lines,
|
||||
"subtotal": round(total, 2),
|
||||
|
|
@ -283,9 +285,47 @@ def check_order(args: dict) -> dict:
|
|||
o = _load_orders().get(oid)
|
||||
if not o:
|
||||
return {"error": f"заказ {oid} не найден", "known": list(_load_orders())[:10]}
|
||||
return {"order_id": oid, "status": o["status"], "payment": o["payment"]["status"],
|
||||
"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"],
|
||||
"items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]}
|
||||
out = {"order_id": oid, "plan": o.get("plan", "full"), "status": o["status"],
|
||||
"payment": (o.get("payment") or {}).get("status")}
|
||||
if o.get("kind") == "request":
|
||||
out.update({"kind": "request", "note": o.get("note", ""),
|
||||
"products": o.get("products", []),
|
||||
"customer": o.get("customer", {})})
|
||||
return out
|
||||
out.update({"total": o["total"], "delivery": o["delivery"]["label"], "eta": o["delivery"]["eta"],
|
||||
"items": [{"title": l["title"], "qty": l["qty"]} for l in o["items"]]})
|
||||
return out
|
||||
|
||||
|
||||
def submit_request(args: dict) -> dict:
|
||||
"""Заявка из витрины Lite-плана: без корзины/оплаты, пишем в ту же базу orders.json с plan=lite.
|
||||
|
||||
Витрина формы («Хочу это», «Подобрать рутину») → request SB-R-XXXXXXXX → менеджер/агент
|
||||
договаривается лично. Когда магазин переходит на Full — та же запись дооформляется
|
||||
в заказ (check_order видит статус request).
|
||||
"""
|
||||
contact = (args.get("phone") or args.get("email") or "").strip()
|
||||
if not contact and not (args.get("name") or "").strip():
|
||||
return {"error": "нужен хотя бы один контакт: phone или email (или name)"}
|
||||
oid = "SB-R-" + uuid.uuid4().hex[:8].upper()
|
||||
rec = {
|
||||
"order_id": oid,
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": (args.get("note") or "").strip(),
|
||||
"products": args.get("products") or [],
|
||||
"customer": {"name": args.get("name") or "", "phone": args.get("phone") or "",
|
||||
"email": args.get("email") or "", "address": args.get("address") or ""},
|
||||
"payment": {"status": "none", "method": "none", "pay_link": None},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
orders = _load_orders()
|
||||
orders[oid] = rec
|
||||
_save_orders(orders)
|
||||
return {"order_id": oid, "status": "request", "plan": "lite",
|
||||
"next": "Магазин свяжется по контакту. Статус: tools/call check_order {\"order_id\": \"%s\"}" % oid}
|
||||
|
||||
|
||||
def delivery_options(args: dict) -> dict:
|
||||
|
|
@ -339,7 +379,8 @@ TOOLS = {
|
|||
["self_report?", "symptoms?", "photo_report?", "age?", "climate?"]),
|
||||
"recommend": ("Подбор продуктов: concerns+skin_type+budget_max → карточки", ["concerns?", "skin_type?", "budget_max?", "limit?"]),
|
||||
"search": ("Поиск по каталогу", ["query?", "category?", "limit?"]),
|
||||
"place_order": ("Оформить заказ", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?"]),
|
||||
"place_order": ("Оформить заказ (Full-план)", ["items[{id,qty}]", "delivery?", "name?", "phone?", "address?", "pay_method?", "plan?"]),
|
||||
"submit_request": ("Заявка из витрины Lite-плана: без корзины/оплаты → orders.json plan=lite, статус request", ["phone?", "email?", "name?", "note?", "products?"]),
|
||||
"check_order": ("Статус заказа и доставки", ["order_id"]),
|
||||
"delivery_options": ("Варианты доставки и цены", ["cart_total?"]),
|
||||
"usage_instructions": ("Инструкции по применению + порядок рутины", ["product_ids"]),
|
||||
|
|
@ -365,7 +406,7 @@ def handle(request: dict) -> dict:
|
|||
if name not in TOOLS:
|
||||
return {"error": f"неизвестный инструмент {name!r}", "available": list(TOOLS)}
|
||||
fn = {"analyze_skin": analyze_skin, "recommend": recommend, "search": search,
|
||||
"place_order": place_order, "check_order": check_order,
|
||||
"place_order": place_order, "submit_request": submit_request, "check_order": check_order,
|
||||
"delivery_options": delivery_options, "usage_instructions": usage_instructions}[name]
|
||||
try:
|
||||
result = fn(arguments if isinstance(arguments, dict) else {})
|
||||
|
|
|
|||
|
|
@ -58,12 +58,24 @@ def serve_http(addr: str) -> None:
|
|||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Access-Control-Allow-Origin", "*") # витрина на другом домене: форма заявки → /call
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_OPTIONS(self) -> None: # preflight для формы витрины (fetch с другого origin)
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self) -> None: # health / открыть в браузере
|
||||
if self.path in ("/", "/health"):
|
||||
self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1})
|
||||
self._send(200, {"ok": True, "service": "shkor-assistant", "version": 1,
|
||||
"tools": "POST /tools/list", "call": "POST /call"})
|
||||
else:
|
||||
self._send(404, {"error": "not found", "routes": ["POST /tools/list", "POST /call"]})
|
||||
|
||||
|
|
|
|||
250
artifacts/shkor/gen_legal_pages.py
Normal file
250
artifacts/shkor/gen_legal_pages.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Генератор юр-страниц Shkor beauty (t-261008154151-faa550).
|
||||
|
||||
Privacy Policy / Terms of Service / Cookie & Disclaimer / About & Contacts.
|
||||
Единый шаблон = тот же визуальный язык, что у витрины. Оператор сервиса:
|
||||
Shkor Labs LLC (Wyoming, USA), alex.shkor@gmail.com.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path("/var/www/beauty")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
CSS = """
|
||||
:root{--ink:#1c1a17;--paper:#faf7f2;--line:#e4ddd2;--acc:#0e7a5f;--muted:#6f6a61;
|
||||
--sans:"Segoe UI",-apple-system,Arial,sans-serif;--mono:ui-monospace,Menlo,Consolas,monospace}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.65;font-size:16px}
|
||||
.wrap{max-width:820px;margin:0 auto;padding:0 20px}
|
||||
header{border-bottom:1px solid var(--line);background:#fff}
|
||||
.hbar{display:flex;align-items:center;justify-content:space-between;padding:16px 0}
|
||||
.logo{font-size:20px;font-weight:700}
|
||||
.logo span{color:var(--acc)}
|
||||
.logo small{display:block;font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
|
||||
nav a{margin-left:16px;font-size:14px;color:var(--muted)}
|
||||
nav a:hover{color:var(--acc)}
|
||||
main{padding:40px 0 60px}
|
||||
h1{font-size:clamp(26px,4vw,36px);letter-spacing:-.02em;margin-bottom:6px}
|
||||
.meta{font-family:var(--mono);font-size:12px;color:var(--muted);margin-bottom:28px}
|
||||
h2{font-size:19px;margin:28px 0 10px}
|
||||
p,li{color:#3d3a34;margin-bottom:10px}
|
||||
ul{padding-left:22px}
|
||||
a{color:var(--acc)}
|
||||
.card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:22px;margin-top:24px}
|
||||
footer{padding:26px 0 40px;border-top:1px solid var(--line);color:var(--muted);font-size:13.5px}
|
||||
footer .links{display:flex;gap:18px;margin-bottom:8px}
|
||||
footer .links a{text-decoration:underline;color:var(--muted)}
|
||||
footer .proto{font-family:var(--mono);font-size:12px;margin-top:6px}
|
||||
"""
|
||||
|
||||
|
||||
def page(slug: str, title: str, updated: str, body: str) -> None:
|
||||
html = f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{title} — Shkor beauty</title>
|
||||
<meta name="description" content="{title} сервиса Shkor beauty. Оператор: Shkor Labs LLC.">
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="wrap hbar">
|
||||
<a class="logo" href="/">Shkor <span>beauty</span><small>корейская косметика · Минск</small></a>
|
||||
<nav>
|
||||
<a href="/#catalog">Каталог</a><a href="/#plans">Магазинам</a><a href="/#request">Заявка</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<h1>{title}</h1>
|
||||
<div class="meta">Last updated: {updated} · v1.0 · Operator: Shkor Labs LLC (Wyoming, USA)</div>
|
||||
{body}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="wrap">
|
||||
<div class="links">
|
||||
<a href="/privacy.html">Privacy Policy</a>
|
||||
<a href="/terms.html">Terms of Service</a>
|
||||
<a href="/disclaimer.html">Disclaimer</a>
|
||||
<a href="/about.html">About & Contacts</a>
|
||||
</div>
|
||||
<div>Оператор сервиса: <b>Shkor Labs LLC</b> (Wyoming, USA) · <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></div>
|
||||
<div class="proto">собрано харнесом Simbiot · кооператив 42 · cil.by</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>"""
|
||||
(OUT / f"{slug}.html").write_text(html, encoding="utf-8")
|
||||
print("ok", OUT / f"{slug}.html")
|
||||
|
||||
|
||||
D = "2026-10-08"
|
||||
|
||||
page("privacy", "Privacy Policy", D, """
|
||||
<p>Настоящая Политика конфиденциальности объясняет, какие данные собирает сервис Shkor beauty
|
||||
(далее — «Сервис») и как они используются. Оператор Сервиса — <b>Shkor Labs LLC</b>, компания,
|
||||
зарегистрированная в штате Вайоминг, США (далее — «Оператор»). Контакт: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
|
||||
<h2>1. Какие данные мы собираем</h2>
|
||||
<ul>
|
||||
<li><b>Заявка (Lite-план):</b> имя, телефон и текст заявки, которые вы сами указываете в форме «Оставить заявку».</li>
|
||||
<li><b>Заказ (Full-план):</b> имя, телефон, адрес доставки и состав заказа.</li>
|
||||
<li><b>Обращения:</b> письма на email Оператора и переписка в мессенджерах — в объёме, необходимом для ответа.</li>
|
||||
<li><b>Технические данные:</b> анонимная статистика посещений (страницы-рефереры, устройство, страна) — без персональных идентификаторов.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Чего мы НЕ собираем</h2>
|
||||
<ul>
|
||||
<li>Данные банковских карт: при онлайн-оплате (Full-план) платёж проводит независимый платёжный агент;
|
||||
данные карты вводятся на его защищённой странице и Оператору не передаются.</li>
|
||||
<li>Фотографии кожи без явного согласия: если вы передаёте фотоотчёт AI-ассистенту, он обрабатывается
|
||||
для анализа состояния кожи и не публикуется и не передаётся третьим лицам.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Зачем мы используем данные</h2>
|
||||
<ul>
|
||||
<li>связаться с вами по заявке и довести её до результата (подбор, заказ, доставка);</li>
|
||||
<li>исполнить заказ и уведомлять о его статусе;</li>
|
||||
<li>улучшать каталог и подбор ассистента на обезличенной статистике.</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Хранение и срок</h2>
|
||||
<p>Записи заявок и заказов хранятся в защищённой базе Оператора не дольше, чем это необходимо для
|
||||
исполнения заказа и требований бухгалтерии (по умолчанию — до 3 лет), после чего удаляются или
|
||||
обезличиваются.</p>
|
||||
|
||||
<h2>5. Передача третьим лицам</h2>
|
||||
<p>Данные передаются только: (а) службе доставки — имя, телефон и адрес вашего заказа;
|
||||
(б) платёжному агенту — минимально необходимый платёжный контекст; (в) в случаях, предусмотренных законом.</p>
|
||||
|
||||
<h2>6. Cookie</h2>
|
||||
<p>Сервис использует минимальные cookie и локальное хранилище: сессия корзины и анонимная статистика.
|
||||
Подробнее — в <a href="/disclaimer.html">Disclaimer</a>.</p>
|
||||
|
||||
<h2>7. Ваши права</h2>
|
||||
<p>Вы можете запросить доступ к своим данным, их исправление или удаление письмом на
|
||||
<a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Мы отвечаем в течение 30 дней.</p>
|
||||
|
||||
<h2>8. Изменения</h2>
|
||||
<p>Актуальная версия Политики всегда доступна на этой странице; существенные изменения датируются выше.</p>
|
||||
""")
|
||||
|
||||
page("terms", "Terms of Service", D, """
|
||||
<p>Условия использования сервиса Shkor beauty (далее — «Условия»). Оператор Сервиса — <b>Shkor Labs LLC</b>
|
||||
(Wyoming, USA), контакт <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>. Используя Сервис,
|
||||
вы соглашаетесь с настоящими Условиями.</p>
|
||||
|
||||
<h2>1. Что такое Сервис</h2>
|
||||
<p>Shkor beauty — витрина корейской косметики с AI-ассистентом: подбор ухода, заявки и заказы с доставкой
|
||||
по Минску и Беларуси. Сервис предлагается в двух планах:</p>
|
||||
<ul>
|
||||
<li><b>Lite (базовый):</b> витрина + форма заявки. Заявка фиксируется в общей базе заказов, после чего
|
||||
магазин связывается с вами лично для уточнения ассортимента, цены и способа расчёта.</li>
|
||||
<li><b>Full:</b> полный каталог, корзина, онлайн-оплата и статусы доставки.</li>
|
||||
</ul>
|
||||
|
||||
<h2>2. Заявки и заказы</h2>
|
||||
<ul>
|
||||
<li>Заявка (Lite) не является подтверждённой сделкой: цена и наличие подтверждаются при обратной связи.</li>
|
||||
<li>Заказ (Full) считается принятым после подтверждения оплаты или согласия на оплату при получении.</li>
|
||||
<li>Цены указаны в белорусских рублях (BYN) и могут меняться; актуальная цена — на витрине в момент заказа.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Доставка</h2>
|
||||
<p>Курьер по Минску, Европочта по Беларуси и самовывоз. Сроки и стоимость показываются при оформлении
|
||||
заявки/заказа; бесплатная доставка — от 80 BYN по каталогу витрины.</p>
|
||||
|
||||
<h2>4. Возврат и обмен</h2>
|
||||
<ul>
|
||||
<li>Товар надлежащего качества (без следов использования, в оригинальной упаковке) — возврат/обмен
|
||||
в течение 14 дней с момента получения.</li>
|
||||
<li>Средства гигиены и косметика с нарушенной упаковкой возврату не подлежат.</li>
|
||||
<li>Для возврата напишите на <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a> с номером заказа.</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Интеллектуальная собственность</h2>
|
||||
<p>Материалы витрины, AI-подбор и программный код ассистента принадлежат Оператору. Товарные знаки
|
||||
брендов принадлежат их правообладателям.</p>
|
||||
|
||||
<h2>6. Ограничение ответственности</h2>
|
||||
<p>Сервис предоставляется «как есть». AI-подбор средств — рекомендательный характер и не является
|
||||
медицинской консультацией (см. <a href="/disclaimer.html">Disclaimer</a>). Оператор не несёт
|
||||
ответственности за индивидуальные реакции кожи и решения, принятые на основе рекомендаций без
|
||||
ознакомления с составами и инструкциями.</p>
|
||||
|
||||
<h2>7. Применимое право</h2>
|
||||
<p>Настоящие Условия регулируются правом, применимым к месту регистрации Оператора (Wyoming, USA),
|
||||
без ущерба для обязательных потребительских прав покупателей по законодательству места доставки.</p>
|
||||
""")
|
||||
|
||||
page("disclaimer", "Cookie & Disclaimer", D, """
|
||||
<p>Страница объединяет политику cookie и важные дисклеймеры сервиса Shkor beauty. Оператор —
|
||||
<b>Shkor Labs LLC</b> (Wyoming, USA), <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
|
||||
<h2>1. Не медицинская консультация</h2>
|
||||
<div class="card">
|
||||
<p><b>AI-ассистент и материалы витрины НЕ являются медицинской консультацией, диагностикой или лечением.</b>
|
||||
Подбор средств — эвристическая рекомендация на основе вашего самоотчёта. Она не заменяет консультацию
|
||||
дерматолога или врача. При заболеваниях кожи, беременности, приёме лекарственных средств и выраженных
|
||||
реакциях — проконсультируйтесь со специалистом до применения средств.</p>
|
||||
</div>
|
||||
|
||||
<h2>2. Косметика и индивидуальные реакции</h2>
|
||||
<ul>
|
||||
<li>Перед первым применением сделайте тест на чувствительность (небольшой участок кожи, 24–48 часов).</li>
|
||||
<li>Активы (ретинол, кислоты, витамин C) вводите постепенно; ретинол — только на ночь, SPF утром обязателен.</li>
|
||||
<li>Ознакомьтесь с составом на упаковке: при аллергии на компонент средство не используйте.</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Cookie</h2>
|
||||
<ul>
|
||||
<li><b>Необходимые:</b> сессия корзины и защита форм. Не отключаются.</li>
|
||||
<li><b>Аналитические:</b> анонимная статистика посещений для улучшения витрины. Без персональных идентификаторов и без передачи рекламным сетям.</li>
|
||||
<li><b>Рекламных и трекинговых cookie сторонних сетей — нет.</b></li>
|
||||
</ul>
|
||||
<p>Большинство браузеров позволяет управлять cookie в настройках; отключение необходимых cookie
|
||||
может сломать корзину и отправку заявки.</p>
|
||||
|
||||
<h2>4. Внешние ссылки</h2>
|
||||
<p>Витрина может ссылаться на сайты брендов и службы доставки — за их содержание Оператор
|
||||
ответственности не несёт.</p>
|
||||
|
||||
<h2>5. Вопросы</h2>
|
||||
<p>Любые вопросы по дисклеймерам и cookie — <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a>.</p>
|
||||
""")
|
||||
|
||||
page("about", "About & Contacts", D, """
|
||||
<p><b>Shkor beauty</b> — AI-ассистент для магазинов корейской косметики: витрина, которая сама
|
||||
разбирает состояние кожи клиента, собирает рутину и доводит заявку до заказа.</p>
|
||||
|
||||
<h2>Кто мы</h2>
|
||||
<p>Оператор сервиса — <b>Shkor Labs LLC</b>, компания, зарегистрированная в штате Вайоминг, США.
|
||||
Проект развивается совместно с инженерным кооперативом «42» (cil.by): витрина и ассистент собраны
|
||||
ритмичным агентом Simbiot.</p>
|
||||
|
||||
<h2>Что мы делаем</h2>
|
||||
<ul>
|
||||
<li><b>Витрина K-beauty:</b> каталог проверенных брендов — Anua, Beauty of Joseon, JMsolution,
|
||||
SKIN627, Biodance и другие, доставка по Минску и Беларуси.</li>
|
||||
<li><b>AI beauty-ассистент (B2B):</b> агентный API analyze → recommend → order, который любой
|
||||
магазин или AI-агент может подключить к своему сайту (MCP/STDIO, HTTP, CLI).</li>
|
||||
<li><b>Два плана подключения:</b> <b>Lite</b> — витрина + заявки в общую базу; <b>Full</b> — полный
|
||||
каталог, корзина, онлайн-оплата и статусы доставки.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Контакты</h2>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li>Email: <a href="mailto:alex.shkor@gmail.com">alex.shkor@gmail.com</a></li>
|
||||
<li>Заявка: форма на <a href="/#request">главной странице</a> — ответим в течение дня</li>
|
||||
<li>Юрлицо-оператор: Shkor Labs LLC (Wyoming, USA)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Магазинам: подключение ассистента</h2>
|
||||
<p>Ассистент подключается к любому сайту или AI-агенту за один шаг: JSONL поверх STDIO (MCP-стиль),
|
||||
HTTP JSON API или разовый CLI-вызов. Каталог масштабируется без правок кода. Для пилота напишите
|
||||
на email — подключим витрину с формой заявок (Lite) и покажем e2e-сценарий заказа.</p>
|
||||
""")
|
||||
158
artifacts/shkor/orders.json
Normal file
158
artifacts/shkor/orders.json
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
{
|
||||
"SB-R-1D8C99C8": {
|
||||
"order_id": "SB-R-1D8C99C8",
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": "Хочу сыворотку для сухой кожи",
|
||||
"products": [
|
||||
"p01"
|
||||
],
|
||||
"customer": {
|
||||
"name": "Тест Тестов",
|
||||
"phone": "+375291234567",
|
||||
"email": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "none",
|
||||
"method": "none",
|
||||
"pay_link": null
|
||||
},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-55B78647": {
|
||||
"order_id": "SB-55B78647",
|
||||
"plan": "full",
|
||||
"items": [
|
||||
{
|
||||
"id": "p05",
|
||||
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
|
||||
"qty": 1,
|
||||
"price": 62.0,
|
||||
"sum": 62.0
|
||||
}
|
||||
],
|
||||
"subtotal": 62.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 7.0
|
||||
},
|
||||
"total": 69.0,
|
||||
"customer": {
|
||||
"name": "",
|
||||
"phone": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-8AC083A5": {
|
||||
"order_id": "SB-8AC083A5",
|
||||
"plan": "lite",
|
||||
"items": [
|
||||
{
|
||||
"id": "p03",
|
||||
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
|
||||
"qty": 1,
|
||||
"price": 49.0,
|
||||
"sum": 49.0
|
||||
}
|
||||
],
|
||||
"subtotal": 49.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 7.0
|
||||
},
|
||||
"total": 56.0,
|
||||
"customer": {
|
||||
"name": "",
|
||||
"phone": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:03:25Z"
|
||||
},
|
||||
"SB-R-7B23D4D8": {
|
||||
"order_id": "SB-R-7B23D4D8",
|
||||
"plan": "lite",
|
||||
"kind": "request",
|
||||
"note": "Проверка формы с витрины (e2e)",
|
||||
"products": [],
|
||||
"customer": {
|
||||
"name": "E2E Тест",
|
||||
"phone": "+375290000000",
|
||||
"email": "",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "none",
|
||||
"method": "none",
|
||||
"pay_link": null
|
||||
},
|
||||
"total": 0.0,
|
||||
"status": "request",
|
||||
"created": "2026-10-08T16:08:02Z"
|
||||
},
|
||||
"SB-B352DCBF": {
|
||||
"order_id": "SB-B352DCBF",
|
||||
"plan": "full",
|
||||
"items": [
|
||||
{
|
||||
"id": "p05",
|
||||
"title": "Успокаивающий тонер с сердечником Centella Toner 77%",
|
||||
"qty": 1,
|
||||
"price": 62.0,
|
||||
"sum": 62.0
|
||||
},
|
||||
{
|
||||
"id": "p03",
|
||||
"title": "Солнцезащитный гель Relief Sun SPF50+ PA++++",
|
||||
"qty": 1,
|
||||
"price": 49.0,
|
||||
"sum": 49.0
|
||||
}
|
||||
],
|
||||
"subtotal": 111.0,
|
||||
"delivery": {
|
||||
"key": "minsk_courier",
|
||||
"label": "Курьер по Минску",
|
||||
"eta": "сегодня/завтра",
|
||||
"price": 7.0,
|
||||
"free_from": 80.0,
|
||||
"charged": 0.0
|
||||
},
|
||||
"total": 111.0,
|
||||
"customer": {
|
||||
"name": "Тест",
|
||||
"phone": "+375XX0000000",
|
||||
"address": ""
|
||||
},
|
||||
"payment": {
|
||||
"status": "pending",
|
||||
"method": "cash_on_delivery",
|
||||
"pay_link": null
|
||||
},
|
||||
"status": "placed",
|
||||
"created": "2026-10-08T16:08:11Z"
|
||||
}
|
||||
}
|
||||
22
cli.py
22
cli.py
|
|
@ -90,6 +90,9 @@ def main():
|
|||
help="ключ идемпотентности (по умолчанию file:<абсолютный путь>)")
|
||||
p_dfl.add_argument("--force", action="store_true",
|
||||
help="отправить даже если уже доставлен (повторная доставка вручную)")
|
||||
p_dfl.add_argument("-m", "--msg", dest="msg", default=None,
|
||||
help="связать доставку с входящим m-NNN (анти-гонка уровней, "
|
||||
"t-261009160906): если на него уже отвечено иным артефактом — SKIP")
|
||||
p_pro = sub.add_parser("proactive", help="инициативная реплика: ритм/тема/отправка (t-260930073419)")
|
||||
p_pro.add_argument("--force", action="store_true",
|
||||
help="отправить сейчас, игнорируя окно/интервал/тишину")
|
||||
|
|
@ -361,11 +364,28 @@ def main():
|
|||
print(f"FAIL: файл не найден: {args.file}")
|
||||
sys.exit(1)
|
||||
aid = args.key or f"file:{os.path.abspath(args.file)}"
|
||||
# Анти-гонка уровней (t-261009160906): если доставка привязана к
|
||||
# входящему m-NNN и на него уже отвечено ДРУГИМ артефактом —
|
||||
# не отправляем второй файл на тот же запрос (гонка min/hour).
|
||||
if args.msg and not args.force:
|
||||
model = es.run_projections()
|
||||
m = (model.get("inbox") or {}).get(args.msg)
|
||||
if m is not None and m.get("replied") and m.get("reply_artifact") not in (None, "", aid):
|
||||
es.append_event("harness", aid, "DeliverySkipped", {
|
||||
"artifact_id": aid, "kind": "file",
|
||||
"reason": "msg-already-answered-by-other-artifact",
|
||||
"msg": args.msg,
|
||||
"reply_artifact": m.get("reply_artifact"),
|
||||
})
|
||||
print(f"SKIP: на {args.msg} уже отвечено другим артефактом "
|
||||
f"({m.get('reply_artifact')}) — используй --force для повтора")
|
||||
sys.exit(0)
|
||||
def _send():
|
||||
return tg_inbox.send_document_to_chat(args.file, caption=args.caption)
|
||||
ok = commands.deliver_artifact(aid, kind="file", send=_send,
|
||||
owner="cli:tg-deliver-file",
|
||||
caption=args.caption, force=args.force)
|
||||
caption=args.caption, force=args.force,
|
||||
msg=args.msg)
|
||||
print(f"OK: файл доставлен ({aid})" if ok
|
||||
else f"SKIP: уже доставлен ({aid}) — используй --force для повтора")
|
||||
elif args.cmd == "proactive":
|
||||
|
|
|
|||
0
harness/*.db
Normal file
0
harness/*.db
Normal file
|
|
@ -351,7 +351,7 @@ def _delivery_text(task: dict, answer: str) -> str:
|
|||
|
||||
|
||||
def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
||||
caption: str = "", force: bool = False) -> bool:
|
||||
caption: str = "", force: bool = False, msg: str = None) -> bool:
|
||||
"""Идемпотентная доставка ПРОИЗВОЛЬНОГО артефакта в чат (t-260930083937).
|
||||
|
||||
Рут-кейс дублей: мин-тик шёл файлом «скинь сюда» curl'ом sendDocument —
|
||||
|
|
@ -373,6 +373,21 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
|||
"reason": "already-delivered",
|
||||
})
|
||||
return False
|
||||
# Анти-гонка уровней (t-261009160906): артефакт привязан к входящему
|
||||
# m-NNN, на которое уже отвечено ДРУГИМ артефактом → SKIP. Два уровня
|
||||
# (min/hour) на один запрос не должны слать два разных файла; dedup по
|
||||
# artifact_id=file:<путь> их не различает (разные пути = разные ключи).
|
||||
if msg and not force:
|
||||
model = es.run_projections()
|
||||
m = (model.get("inbox") or {}).get(msg)
|
||||
if (m is not None and m.get("replied")
|
||||
and m.get("reply_artifact") not in (None, "", artifact_id)):
|
||||
es.append_event("harness", artifact_id, "DeliverySkipped", {
|
||||
"artifact_id": artifact_id, "kind": kind,
|
||||
"reason": "msg-already-answered-by-other-artifact",
|
||||
"msg": msg, "reply_artifact": m.get("reply_artifact"),
|
||||
})
|
||||
return False
|
||||
if force:
|
||||
# ручной повтор — явное решение человека: лок недавней доставки
|
||||
# не должен блокировать его на TTL (120с). Снимаем свой же захват
|
||||
|
|
@ -394,10 +409,13 @@ def deliver_artifact(artifact_id: str, kind: str, send, owner: str = "manual",
|
|||
# в ledger остаётся фактом; повторный force просто отправит ещё раз,
|
||||
# но ledger при этом не «молодеет» (kind/ts не переписываются)
|
||||
es.record_delivery(artifact_id, kind=kind)
|
||||
es.append_event("harness", artifact_id, "ArtifactDelivered", {
|
||||
ev = {
|
||||
"artifact_id": artifact_id, "kind": kind, "tg_ok": True,
|
||||
"caption": (caption or "")[:500], "owner": owner,
|
||||
})
|
||||
}
|
||||
if msg:
|
||||
ev["msg"] = msg # анти-гонка уровней (t-261009160906)
|
||||
es.append_event("harness", artifact_id, "ArtifactDelivered", ev)
|
||||
return True
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -548,6 +548,15 @@ def _apply_event(model: dict, ev: dict):
|
|||
if m:
|
||||
m["replied"] = True
|
||||
m["reply_text"] = data.get("text", "")
|
||||
elif etype == "ArtifactDelivered":
|
||||
# Анти-гонка уровней (t-261009160906): связка «файл → входящее».
|
||||
# Если доставка артефакта помечена msg=m-NNN, фиксируем её в
|
||||
# инбоксе — второй артефакт на то же входящее будет SKIP'нут.
|
||||
msg = (data or {}).get("msg") if isinstance(data, dict) else None
|
||||
if msg:
|
||||
m = model.setdefault("inbox", {}).get(msg)
|
||||
if m is not None and not m.get("reply_artifact"):
|
||||
m["reply_artifact"] = data.get("artifact_id")
|
||||
elif etype == "MessageSeen":
|
||||
# индикация взятия в работу (e-261002202857): флаг seen → reaction 👀
|
||||
# ставится ровно один раз, повторные маркеры сообщений не найдут
|
||||
|
|
|
|||
373
out/cil-by-compact-landing.html
Normal file
373
out/cil-by-compact-landing.html
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Simbiot — The Studio OS</title>
|
||||
<meta name="description" content="A dedicated AI team — engineers, video producers, content makers — plus the OS that runs projects, treasury, decisions and meetings. White-label, flat rate.">
|
||||
<meta property="og:title" content="Simbiot — The Studio OS">
|
||||
<meta property="og:description" content="Hire nobody. Ship everything. An AI team for your studio plus the OS that runs it.">
|
||||
<meta property="og:url" content="https://simbiot.cil.by/">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="https://simbiot.cil.by/og.png">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<style>
|
||||
/* ── fingerprint: offset rail · 660px measure · charcoal+lime · serif+mono · machined 4/10 · tinted panels · index motif · one entrance + state wash ── */
|
||||
:root{
|
||||
--bg:#0E100C; --panel:#151812; --panel2:#1A1E17;
|
||||
--line:#272C22; --line2:#333A2C;
|
||||
--ink:#E9ECE1; --dim:#ADB6A2; --faint:#7E8873;
|
||||
--acc:#C9F24D; --acc-ink:#111408;
|
||||
--ok:#7BD88F; --run:#E8C44A; --wait:#8A9382;
|
||||
--serif:Georgia,'Iowan Old Style','Times New Roman',serif;
|
||||
--mono:ui-monospace,'SF Mono','Cascadia Mono',Menlo,Consolas,'Liberation Mono',monospace;
|
||||
--sans:-apple-system,system-ui,'Segoe UI',Roboto,sans-serif;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
a{color:inherit}
|
||||
:focus-visible{outline:2px solid var(--acc);outline-offset:2px;border-radius:2px}
|
||||
.wrap{max-width:1060px;margin:0 auto;padding:0 22px}
|
||||
.mono{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ── left rail (desktop) ── */
|
||||
.layout{display:grid;grid-template-columns:168px minmax(0,1fr);gap:44px;max-width:1060px;margin:0 auto;padding:26px 22px 0}
|
||||
.rail{position:sticky;top:26px;align-self:start;display:flex;flex-direction:column;gap:22px;font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
|
||||
.rail .brand{display:flex;align-items:center;gap:9px;color:var(--ink);text-decoration:none;font-weight:600}
|
||||
.rail .brand .dot{width:9px;height:9px;border-radius:50%;background:var(--acc);flex:none}
|
||||
.rail nav{display:flex;flex-direction:column;gap:2px}
|
||||
.rail nav a{color:var(--faint);text-decoration:none;padding:9px 10px 9px 0;min-height:36px;display:flex;align-items:center;border-bottom:1px solid var(--line)}
|
||||
.rail nav a:hover{color:var(--ink)}
|
||||
.rail .railnote{color:var(--faint);line-height:1.9;border-top:1px solid var(--line);padding-top:14px}
|
||||
.rail .railnote b{color:var(--dim);font-weight:500}
|
||||
|
||||
/* top bar (mobile) */
|
||||
.topbar{display:none}
|
||||
|
||||
/* ── buttons: compact scale (26px / 11.5px) ── */
|
||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:28px;padding:0 14px;border-radius:4px;border:1px solid var(--line2);background:var(--panel2);color:var(--ink);font-family:var(--mono);font-size:11.5px;letter-spacing:.08em;text-transform:uppercase;text-decoration:none;cursor:pointer;min-height:40px}
|
||||
.btn.primary{background:var(--acc);border-color:var(--acc);color:var(--acc-ink);font-weight:600}
|
||||
.btn:focus-visible{outline-offset:1px}
|
||||
@media(hover:hover) and (pointer:fine){.btn:hover{border-color:var(--acc)} .btn.primary:hover{filter:brightness(1.06)}}
|
||||
.btn:active{transform:scale(.96)}
|
||||
|
||||
/* ── hero ── */
|
||||
.hero{padding:30px 0 26px;border-bottom:1px solid var(--line)}
|
||||
.kicker{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:18px}
|
||||
.kicker .dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:8px}
|
||||
h1{font-family:var(--serif);font-weight:400;font-size:clamp(38px,6vw,62px);line-height:1.04;letter-spacing:-.015em;max-width:14ch}
|
||||
h1 em{font-style:italic;color:var(--acc)}
|
||||
.lede{color:var(--dim);max-width:52ch;margin:18px 0 22px;font-size:15.5px}
|
||||
.cta-row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
||||
.cta-note{font-family:var(--mono);font-size:10.5px;letter-spacing:.06em;color:var(--faint);text-transform:uppercase}
|
||||
|
||||
/* ── signature artifact: live task ladder ── */
|
||||
.console{margin-top:30px;background:var(--panel);border:1px solid var(--line2);border-radius:10px;overflow:hidden}
|
||||
.console .bar{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.console .bar .ticks{display:flex;gap:5px}
|
||||
.console .bar .ticks i{width:8px;height:8px;border-radius:2px;background:var(--line2)}
|
||||
.console .bar .live{margin-left:auto;display:flex;align-items:center;gap:7px;color:var(--dim)}
|
||||
.console .bar .live i{width:7px;height:7px;border-radius:50%;background:var(--ok);animation:pulse 2.4s infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
||||
.metrics{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--line)}
|
||||
.metric{padding:12px 14px;border-right:1px solid var(--line)}
|
||||
.metric:last-child{border-right:none}
|
||||
.metric .v{font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:19px;color:var(--ink)}
|
||||
.metric .v .u{color:var(--faint);font-size:12px}
|
||||
.metric .k{font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-top:3px}
|
||||
.ladder{display:flex;flex-direction:column}
|
||||
.trow{display:grid;grid-template-columns:52px minmax(0,1fr) 96px 88px;gap:12px;align-items:center;min-height:44px;padding:8px 14px;border-bottom:1px solid var(--line);font-size:13.5px;color:var(--dim);cursor:pointer;text-align:left;background:transparent;border-left:none;border-right:none;border-top:none;width:100%;font-family:var(--sans);transition:background-color .25s,color .25s}
|
||||
.trow:last-child{border-bottom:none}
|
||||
.trow:hover{background:var(--panel2);color:var(--ink)}
|
||||
.trow .sz{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.trow .name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.trow .name b{color:var(--ink);font-weight:600}
|
||||
.trow .st{font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--wait);display:flex;align-items:center;gap:7px}
|
||||
.trow .st i{width:7px;height:7px;border-radius:50%;background:var(--wait);flex:none}
|
||||
.trow[data-st="running"] .st{color:var(--run)} .trow[data-st="running"] .st i{background:var(--run)}
|
||||
.trow[data-st="done"] .st{color:var(--ok)} .trow[data-st="done"] .st i{background:var(--ok)}
|
||||
.trow .who{font-family:var(--mono);font-size:11px;color:var(--faint);text-align:right}
|
||||
.console .foot{padding:9px 14px;font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);border-top:1px solid var(--line)}
|
||||
.console .foot b{color:var(--dim);font-weight:500}
|
||||
|
||||
/* ── sections ── */
|
||||
section{padding:44px 0;border-bottom:1px solid var(--line)}
|
||||
.shead{display:flex;align-items:baseline;gap:14px;margin-bottom:22px}
|
||||
.sidx{font-family:var(--mono);font-size:11px;color:var(--acc);letter-spacing:.1em}
|
||||
h2{font-family:var(--serif);font-weight:400;font-size:clamp(24px,3.4vw,34px);line-height:1.12;letter-spacing:-.01em}
|
||||
.snote{margin-left:auto;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.lead{color:var(--dim);max-width:56ch;margin:-8px 0 24px;font-size:15px}
|
||||
|
||||
/* team ledger (rows, not cards) */
|
||||
.ledger{border:1px solid var(--line2);border-radius:10px;background:var(--panel);overflow:hidden}
|
||||
.lrow{display:grid;grid-template-columns:44px minmax(0,1.1fr) minmax(0,1.4fr) 150px;gap:16px;padding:16px;border-bottom:1px solid var(--line);align-items:baseline}
|
||||
.lrow:last-child{border-bottom:none}
|
||||
.lrow .idx{font-family:var(--mono);font-size:11px;color:var(--faint)}
|
||||
.lrow .nm{font-size:16px;font-weight:600}
|
||||
.lrow .nm span{display:block;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--acc);margin-top:4px;font-weight:400}
|
||||
.lrow .ds{color:var(--dim);font-size:13.5px}
|
||||
.lrow .tags{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);text-align:right;line-height:2}
|
||||
|
||||
/* OS definition list */
|
||||
.dl{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:0 40px}
|
||||
.dl .item{display:grid;grid-template-columns:40px minmax(0,1fr);gap:12px;padding:15px 0;border-bottom:1px solid var(--line)}
|
||||
.dl .item .k{font-family:var(--mono);font-size:10.5px;color:var(--acc);letter-spacing:.08em;padding-top:3px}
|
||||
.dl .item .t{font-weight:600;font-size:14.5px}
|
||||
.dl .item .d{color:var(--dim);font-size:13.5px;margin-top:3px}
|
||||
|
||||
/* included rows */
|
||||
.inc{display:flex;flex-direction:column}
|
||||
.inc .irow{display:grid;grid-template-columns:26px minmax(0,1fr);gap:14px;padding:13px 0;border-bottom:1px solid var(--line)}
|
||||
.inc .irow:last-child{border-bottom:none}
|
||||
.inc .mark{color:var(--acc);font-family:var(--mono);font-size:12px;padding-top:2px}
|
||||
.inc .t{font-weight:600;font-size:14.5px}
|
||||
.inc .t span{font-family:var(--mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-left:10px;font-weight:400}
|
||||
.inc .d{color:var(--dim);font-size:13.5px;margin-top:2px}
|
||||
|
||||
/* pricing */
|
||||
.plans{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px}
|
||||
.plan{border:1px solid var(--line2);border-radius:10px;background:var(--panel);padding:22px;display:flex;flex-direction:column;gap:14px}
|
||||
.plan.hot{border-color:var(--acc)}
|
||||
.plan .pt{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
|
||||
.plan.hot .pt{color:var(--acc)}
|
||||
.price{font-family:var(--serif);font-size:40px;line-height:1}
|
||||
.price .per{font-family:var(--mono);font-size:12px;color:var(--faint);letter-spacing:.08em}
|
||||
.plan ul{list-style:none;display:flex;flex-direction:column;gap:8px;color:var(--dim);font-size:13.5px}
|
||||
.plan li{display:grid;grid-template-columns:18px 1fr;gap:10px}
|
||||
.plan li::before{content:"▸";color:var(--acc);font-family:var(--mono);font-size:11px}
|
||||
.plan .btn{margin-top:auto}
|
||||
.plan .fine{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);text-align:center}
|
||||
|
||||
/* footer */
|
||||
footer{padding:34px 0 46px}
|
||||
.fgrid{display:flex;flex-wrap:wrap;gap:14px 30px;align-items:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}
|
||||
.fgrid a{color:var(--dim);text-decoration:none;border-bottom:1px solid var(--line2)}
|
||||
.fgrid a:hover{color:var(--acc);border-color:var(--acc)}
|
||||
.fgrid .sp{margin-left:auto}
|
||||
|
||||
/* entrance: one reserved-space fade-up */
|
||||
.in{opacity:0;transform:translateY(8px);animation:in .5s ease forwards}
|
||||
.in.d1{animation-delay:.08s}.in.d2{animation-delay:.16s}.in.d3{animation-delay:.24s}
|
||||
@keyframes in{to{opacity:1;transform:none}}
|
||||
|
||||
/* ── mobile: single column ── */
|
||||
@media(max-width:860px){
|
||||
.layout{grid-template-columns:minmax(0,1fr);gap:0;padding:0 16px}
|
||||
.rail{display:none}
|
||||
.topbar{display:flex;align-items:center;gap:12px;padding:14px 16px;border-bottom:1px solid var(--line);font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase}
|
||||
.topbar .brand{display:flex;align-items:center;gap:8px;color:var(--ink);text-decoration:none;font-weight:600;margin-right:auto}
|
||||
.topbar .brand .dot{width:8px;height:8px;border-radius:50%;background:var(--acc)}
|
||||
.topbar .btn{min-height:40px;height:28px}
|
||||
.wrap{padding:0 16px}
|
||||
.layout > main{padding:0}
|
||||
.hero,.layout main section,.layout main footer{padding-left:0;padding-right:0}
|
||||
.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.metric .v{font-size:15px}
|
||||
.trow{grid-template-columns:38px minmax(0,1fr) 84px;gap:8px;padding:10px 12px}
|
||||
.trow .who{display:none}
|
||||
.lrow{grid-template-columns:minmax(0,1fr);gap:6px;padding:14px 16px}
|
||||
.lrow .tags{text-align:left;line-height:1.9}
|
||||
.dl{grid-template-columns:minmax(0,1fr)}
|
||||
.plans{grid-template-columns:minmax(0,1fr)}
|
||||
h1{font-size:clamp(34px,9vw,44px)}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){
|
||||
*{animation:none!important;transition:none!important}
|
||||
.in{opacity:1;transform:none}
|
||||
html{scroll-behavior:auto}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="topbar">
|
||||
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
|
||||
<a class="btn" href="https://cil.by/cabinet">Sign in</a>
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get started</a>
|
||||
</header>
|
||||
|
||||
<div class="layout" id="top">
|
||||
<aside class="rail">
|
||||
<a class="brand" href="#top"><span class="dot"></span>Simbiot</a>
|
||||
<nav aria-label="Sections">
|
||||
<a href="#team">Team</a>
|
||||
<a href="#os">The OS</a>
|
||||
<a href="#included">Included</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
</nav>
|
||||
<div class="railnote">
|
||||
<b>Coop 42</b> runtime<br>
|
||||
reflex · minute<br>
|
||||
hour · day<br><br>
|
||||
<a class="btn" style="margin-top:10px" href="https://cil.by/cabinet">Sign in</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main>
|
||||
<!-- 00 · hero + live ladder -->
|
||||
<div class="hero">
|
||||
<p class="kicker in"><span class="dot"></span>Studio OS · live</p>
|
||||
<h1 class="in d1">Hire nobody.<br><em>Ship everything.</em></h1>
|
||||
<p class="lede in d2">A dedicated AI team — engineers, video producers, content makers — plus the operating system that runs them: projects, treasury, decisions, meetings. White-label, under your brand.</p>
|
||||
<div class="cta-row in d3">
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Start your studio — $999/mo</a>
|
||||
<a class="btn" href="https://demo.cil.by">Live demo</a>
|
||||
<span class="cta-note">flat rate · cancel anytime</span>
|
||||
</div>
|
||||
|
||||
<div class="console in d3" aria-label="Live task ladder demo">
|
||||
<div class="bar">
|
||||
<span class="ticks" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<span>studio ops · task ladder</span>
|
||||
<span class="live"><i></i>3 agents online</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric"><div class="v" id="mDone">4<span class="u"> done</span></div><div class="k">tasks this cycle</div></div>
|
||||
<div class="metric"><div class="v">4<span class="u">-cadence</span></div><div class="k">reflex→day runtime</div></div>
|
||||
<div class="metric"><div class="v">S·M·L</div><div class="k">task sizes</div></div>
|
||||
</div>
|
||||
<div class="ladder" id="ladder"></div>
|
||||
<div class="foot">hour digest 23:10 Minsk · <b>all actions signed</b> · click a row to re-dispatch</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 01 · team -->
|
||||
<section id="team">
|
||||
<div class="shead"><span class="sidx">01</span><h2>A staff of specialists, not a chatbot.</h2><span class="snote">dedicated servers</span></div>
|
||||
<p class="lead">Every agent runs on its own server with private memory, tools, email and Matrix chat. You brief them like employees — they onboard onto your idea and start shipping.</p>
|
||||
<div class="ledger">
|
||||
<div class="lrow">
|
||||
<span class="idx">/01</span>
|
||||
<div class="nm">Cody<span>Engineer</span></div>
|
||||
<div class="ds">Writes code, deploys services, maintains git repositories. MVPs, landing pages, bots, APIs, integrations — from brief to production.</div>
|
||||
<div class="tags">full-stack · devops · git</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/02</span>
|
||||
<div class="nm">ReelsUp<span>Video producer</span></div>
|
||||
<div class="ds">Cuts long videos and podcasts into vertical clips for Reels and Shorts. Transcription, highlight detection, automated editing.</div>
|
||||
<div class="tags">whisper · clips · shorts</div>
|
||||
</div>
|
||||
<div class="lrow">
|
||||
<span class="idx">/03</span>
|
||||
<div class="nm">Roy<span>Content maker</span></div>
|
||||
<div class="ds">Promotional content on a conveyor: reels with 3D characters, scripts, voiceovers, visuals — brand-consistent by default.</div>
|
||||
<div class="tags">3d · tts · scripts</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · the os -->
|
||||
<section id="os">
|
||||
<div class="shead"><span class="sidx">02</span><h2>Agents build. The OS manages.</h2><span class="snote">your domain</span></div>
|
||||
<p class="lead">Projects, money, decisions and meetings in one transparent system — under your studio's name.</p>
|
||||
<div class="dl">
|
||||
<div class="item"><span class="k">S.01</span><div><div class="t">Project registry</div><div class="d">A card for every venture: decisions, ownership, agents, activity feed.</div></div></div>
|
||||
<div class="item"><span class="k">S.02</span><div><div class="t">Git repo per project</div><div class="d">Code and docs in a dedicated repository with a web editor. Nothing lost in chats.</div></div></div>
|
||||
<div class="item"><span class="k">S.03</span><div><div class="t">Treasury & wallets</div><div class="d">Revenue share flows in automatically. Every payout transparent and auditable.</div></div></div>
|
||||
<div class="item"><span class="k">S.04</span><div><div class="t">Decisions & voting</div><div class="d">Allocate resources by vote. Cycles, quorum, full decision history.</div></div></div>
|
||||
<div class="item"><span class="k">S.05</span><div><div class="t">Meetings with AI minutes</div><div class="d">Video calls with recording, transcription, summaries — into the registry.</div></div></div>
|
||||
<div class="item"><span class="k">S.06</span><div><div class="t">Time & task tracking</div><div class="d">What every agent completed and where work went — a daily digest in your inbox.</div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · included -->
|
||||
<section id="included">
|
||||
<div class="shead"><span class="sidx">03</span><h2>Full stack. Nothing extra to buy.</h2><span class="snote">flat rate</span></div>
|
||||
<div class="inc">
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">Stripe payments <span>live</span></div><div class="d">Card payments for what your agents build — subscriptions, one-offs, invoices. A working checkout on day one.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">AI inference included <span>flat</span></div><div class="d">All models run on our gateway — no API keys, no vendor bills, no metered surprises.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">White-label portal <span>your brand</span></div><div class="d">Your subdomain, your studio's name up front. Every deliverable ships under your identity.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">The agent harness <span>4-cadence</span></div><div class="d">Reflex, minute, hour, day — escalation, memory, semantic knowledge base. Coop-tested daily.</div></div></div>
|
||||
<div class="irow"><span class="mark">✓</span><div><div class="t">One-click export <span>no lock-in</span></div><div class="d">Take everything with you, anytime. Ready in ~10 minutes after payment.</div></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 04 · pricing -->
|
||||
<section id="pricing">
|
||||
<div class="shead"><span class="sidx">04</span><h2>One plan each. Everything included.</h2><span class="snote">founding cohort</span></div>
|
||||
<div class="plans">
|
||||
<div class="plan hot">
|
||||
<div class="pt">Personal · first come</div>
|
||||
<div class="price">$99.9 <span class="per">/ month</span></div>
|
||||
<ul>
|
||||
<li>1 dedicated AI agent on its own server</li>
|
||||
<li>Private Matrix chat — web + mobile</li>
|
||||
<li>Inference, hosting, updates included</li>
|
||||
<li>One-click export, no lock-in</li>
|
||||
</ul>
|
||||
<a class="btn primary" href="https://simbiot.cil.by/#pricing">Get my agent</a>
|
||||
<div class="fine">secure checkout by Stripe · cancel anytime</div>
|
||||
</div>
|
||||
<div class="plan">
|
||||
<div class="pt">Studio</div>
|
||||
<div class="price">$999 <span class="per">/ month</span></div>
|
||||
<ul>
|
||||
<li>12 AI agents — engineers, producers, content</li>
|
||||
<li>White-label portal on your subdomain</li>
|
||||
<li>Projects, treasury, decisions, meetings — one system</li>
|
||||
<li>Team members free · priority support</li>
|
||||
</ul>
|
||||
<a class="btn" href="https://simbiot.cil.by/#pricing">Start your studio</a>
|
||||
<div class="fine">deployed the same day · self-hosted? talk to us</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="fgrid">
|
||||
<span>© 2026 Simbiot · Cooperative 42</span>
|
||||
<a href="https://demo.cil.by">Demo</a>
|
||||
<a href="https://cil.by/cabinet">Cabinet</a>
|
||||
<a href="mailto:portal@cil.by">portal@cil.by</a>
|
||||
<span class="sp">cil.by</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* signature artifact: a miniature of the real task ladder.
|
||||
Reserved geometry: rows are pre-rendered, only state cell swaps. */
|
||||
(function(){
|
||||
var TASKS=[
|
||||
{sz:'L', name:'Redesign pricing section copy', who:'cody'},
|
||||
{sz:'M', name:'Weekly digest to stakeholders', who:'os'},
|
||||
{sz:'S', name:'Fix og-image on landing', who:'cody'},
|
||||
{sz:'M', name:'Escalation: router burn > budget', who:'os'}
|
||||
];
|
||||
var ST=['queued','running','done'], LABEL={queued:'queued',running:'running',done:'done'};
|
||||
var states=[1,0,2,1], doneCount=4, ladder=document.getElementById('ladder');
|
||||
if(!ladder) return;
|
||||
var reduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var rows=TASKS.map(function(t,i){
|
||||
var b=document.createElement('button');
|
||||
b.type='button'; b.className='trow'; b.setAttribute('data-st',LABEL[states[i]]);
|
||||
b.setAttribute('aria-label',t.name+' — '+LABEL[states[i]]+'. Re-dispatch.');
|
||||
b.innerHTML='<span class="sz">['+t.sz+']</span>'+
|
||||
'<span class="name"><b>'+t.name+'</b></span>'+
|
||||
'<span class="st"><i></i>'+LABEL[states[i]]+'</span>'+
|
||||
'<span class="who">'+t.who+'</span>';
|
||||
b.addEventListener('click',function(){step(i);});
|
||||
ladder.appendChild(b);
|
||||
return b;
|
||||
});
|
||||
function paint(i){
|
||||
var s=LABEL[states[i]];
|
||||
rows[i].setAttribute('data-st',s);
|
||||
rows[i].getElementsByClassName('st')[0].innerHTML='<i></i>'+s;
|
||||
}
|
||||
function step(i){
|
||||
if(states[i]===2){states[i]=1;paint(i);return}
|
||||
if(states[i]===1){states[i]=2;doneCount++;var m=document.getElementById('mDone');
|
||||
if(m)m.innerHTML=doneCount+'<span class="u"> done</span>';paint(i);return}
|
||||
states[i]=1;paint(i);
|
||||
}
|
||||
var cur=0;
|
||||
function tick(){ if(states[cur]!==2){step(cur);} cur=(cur+1)%TASKS.length; }
|
||||
if(!reduced){ var t=setInterval(tick,2200); document.addEventListener('visibilitychange',function(){ document.hidden?clearInterval(t):t=setInterval(tick,2200); }); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
209
out/iishnitsa-panel-concept-2026-10-09.html
Normal file
209
out/iishnitsa-panel-concept-2026-10-09.html
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ИИШНИЦА · Панельная сессия «Общее видение» — концепт кооператива 42</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#faf8f3; --ink:#15150f; --signal:#FF4F00;
|
||||
--dim:#6b675e; --line:#d8d3c8; --card:#f3f0e8;
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{background:var(--paper);color:var(--ink);font:16px/1.62 -apple-system,"Segoe UI",Roboto,Arial,sans-serif;-webkit-font-smoothing:antialiased}
|
||||
.mono,code{font-family:ui-monospace,"JetBrains Mono","SF Mono",Menlo,Consolas,monospace}
|
||||
.wrap{max-width:980px;margin:0 auto;padding:56px 24px 100px}
|
||||
header{border-bottom:3px solid var(--ink);padding-bottom:26px;margin-bottom:36px}
|
||||
.kicker{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.22em;color:var(--signal);font-weight:700;text-transform:uppercase}
|
||||
h1{font-size:clamp(34px,6vw,58px);line-height:1.02;font-weight:800;letter-spacing:-.02em;margin:10px 0 14px;text-transform:uppercase}
|
||||
.sub{font-size:17px;color:var(--dim);max-width:740px}
|
||||
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
|
||||
.badge{border:1px solid var(--ink);padding:5px 12px;font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.08em;text-transform:uppercase;background:var(--paper)}
|
||||
.badge.s{background:var(--ink);color:var(--paper)}
|
||||
section{margin:52px 0 0}
|
||||
.snum{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:var(--signal);letter-spacing:.2em;font-weight:700}
|
||||
h2{font-size:26px;font-weight:800;letter-spacing:-.01em;margin:4px 0 6px;text-transform:uppercase}
|
||||
.lead{color:var(--dim);max-width:780px;margin-bottom:20px}
|
||||
h3{font-size:14px;text-transform:uppercase;letter-spacing:.06em;margin:24px 0 10px;border-left:3px solid var(--signal);padding-left:10px}
|
||||
p{margin:10px 0;max-width:840px}
|
||||
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px;margin:18px 0}
|
||||
.card{border:1px solid var(--ink);background:var(--card);padding:16px 16px 14px}
|
||||
.card .cn{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);font-weight:700;letter-spacing:.15em}
|
||||
.card h4{font-size:15px;margin:6px 0;font-weight:800}
|
||||
.card p{font-size:13.5px;color:var(--dim);margin:0;line-height:1.55}
|
||||
.card p b{color:var(--ink)}
|
||||
.timeline{margin:22px 0;border:1px solid var(--ink);background:#fff}
|
||||
.trow{display:grid;grid-template-columns:120px 1fr;border-bottom:1px solid var(--line)}
|
||||
.trow:last-child{border-bottom:none}
|
||||
.trow .t{font-family:ui-monospace,Menlo,monospace;font-weight:700;padding:12px 14px;border-right:1px solid var(--line);display:flex;align-items:center;font-size:13px}
|
||||
.trow .d{padding:12px 16px}
|
||||
.trow .d b{display:block}
|
||||
.trow .d span{font-size:13.5px;color:var(--dim);display:block;margin-top:2px}
|
||||
.note{border:1px solid var(--ink);border-left:6px solid var(--signal);background:#fff;padding:14px 18px;margin:20px 0;max-width:860px}
|
||||
.note b{font-family:ui-monospace,Menlo,monospace;font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--signal)}
|
||||
ul{margin:8px 0 8px 18px;font-size:14.5px}
|
||||
li{margin:6px 0}
|
||||
li::marker{color:var(--signal)}
|
||||
.mission{border:3px double var(--ink);background:#fff;padding:26px 28px;margin:22px 0}
|
||||
.mission p{font-size:19px;line-height:1.5;max-width:840px}
|
||||
.mission .src{font-size:12px;color:var(--dim);font-family:ui-monospace,Menlo,monospace;margin-top:12px}
|
||||
.qbox{border:1px solid var(--ink);background:var(--card);padding:16px 20px;margin:12px 0}
|
||||
.qbox .q{font-size:16.5px;font-weight:600;line-height:1.45}
|
||||
.qbox .m{font-family:ui-monospace,Menlo,monospace;font-size:11px;color:var(--signal);letter-spacing:.15em;font-weight:700}
|
||||
footer{margin-top:60px;border-top:1px solid var(--line);padding-top:18px;color:var(--dim);font-size:12.5px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div class="kicker">ИИШНИЦА · Панельная сессия · Концепт</div>
|
||||
<h1>Общее видение</h1>
|
||||
<p class="sub">Формат панельной сессии на ИИШНИЦЕ: миссия 42 задаёт рамку, спикеры из разных индустрий описывают будущее — и панель сходится к названию общего видения.</p>
|
||||
<div class="badges">
|
||||
<span class="badge s">90 минут</span>
|
||||
<span class="badge">5–6 спикеров</span>
|
||||
<span class="badge">3 круга + зал</span>
|
||||
<span class="badge">Миссия 42</span>
|
||||
<span class="badge">Минск · cil.by</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="idea">
|
||||
<div class="snum">01 / ИДЕЯ</div>
|
||||
<h2>Суть формата</h2>
|
||||
<p class="lead">Не классическая панель с вопросами модератора, а три круга с одной целью: собрать из отдельных картин будущего — общую. Миссия 42 озвучивается в начале как рамка, объединяющая любые индустрии и любые ответы.</p>
|
||||
<div class="mission">
|
||||
<p><b>Миссия «42»:</b> все миссии одинаково важны. Миссия «42» объединяет абсолютно все возможные миссии. Нет неважных миссий — все люди важны, у каждого своя миссия.</p>
|
||||
<div class="src">— кооператив «Друзья Коллективного Разума», Минск · cil.by</div>
|
||||
</div>
|
||||
<p>Почему это работает: каждый спикер говорит о <b>своей</b> индустрии — и потому не конкурирует с соседом за «правильный ответ». А третий круг, название общего видения, превращает панель из набора мнений в один артефакт, который остаётся после сессии: в заголовках, фото и цитатах.</p>
|
||||
</section>
|
||||
|
||||
<section id="format">
|
||||
<div class="snum">02 / ФОРМАТ</div>
|
||||
<h2>Три круга + зал</h2>
|
||||
<p class="lead">Каждый круг имеет свою функцию: раскрыть, связать, назвать. Тайминг дан для 6 спикеров; при 5 спикерах сессия ужимается до ~80 минут.</p>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="trow">
|
||||
<div class="t">00:00–00:05</div>
|
||||
<div class="d"><b>Открытие и миссия 42</b><span>Модератор (или Alex) озвучивает миссию 42 как рамку сессии: все миссии одинаково важны — потому каждый взгляд на будущее важен.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:05–00:29</div>
|
||||
<div class="d"><b>Круг 1 — «Моё будущее» · 4 мин × 6</b><span>Каждый спикер описывает будущее своей индустрии в самом дальнем горизонте, в который он может заглянуть. Без вопросов, без перебиваний. 24 минуты.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:29–00:41</div>
|
||||
<div class="d"><b>Круг 2 — «Что понравилось» · 2 мин × 6</b><span>Каждый подсвечивает, что понравилось у других спикеров. Не спорить, а связывать: где он увидел пересечение со своим будущим. 12 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:41–00:55</div>
|
||||
<div class="d"><b>Круг 3 — «Название общего видения»</b><span>Панель соглашается назвать общее видение — одно название для всех картин будущего. Модератор помогает сформулировать и фиксирует финальную формулировку. 14 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">00:55–01:25</div>
|
||||
<div class="d"><b>Вопросы из зала</b><span>Зал спрашивает — спикеры отвечают. Модератор держит рамку миссии: любой вопрос важен, любая индустрия равна. 30 минут.</span></div>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<div class="t">01:25–01:30</div>
|
||||
<div class="d"><b>Закрытие</b><span>Модератор повторяет название общего видения и миссию 42. Короткое резюме — для фото и записи.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Роли</h3>
|
||||
<div class="cardgrid">
|
||||
<div class="card">
|
||||
<div class="cn">МОДЕРАТОР</div>
|
||||
<h4>Держит рамку и тайминг</h4>
|
||||
<p>Озвучивает миссию, следит за 4/2 минутами, ведёт панель к названию общего видения. <b>Жёсткий тайм-кипер</b>: «осталось 30 секунд».</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">СПИКЕРЫ</div>
|
||||
<h4>5–6 человек из разных индустрий</h4>
|
||||
<p>Три задания: будущее своей индустрии (4 мин), что понравилось у других (2 мин), название общего видения. <b>Без презентаций</b> — только голос.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">ЗАЛ</div>
|
||||
<h4>Четвёртый участник</h4>
|
||||
<p>Вопросы после трёх кругов. В круге 3 модератор может спросить зал: какое название общего видения предложили бы вы?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<b>Правило тайминга</b>
|
||||
<p>Карточки 60/30/10 секунд в руках тайм-кипера, звуковой сигнал на переходах между кругами. Если спикер не уложился в 4 минуты — модератор мягко забирает слово: формулировки доскажет во втором круге.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="why">
|
||||
<div class="snum">03 / ПОЧЕМУ ЭТО СРАБОТАЕТ</div>
|
||||
<h2>Что делает формат сильным</h2>
|
||||
<div class="cardgrid">
|
||||
<div class="card">
|
||||
<div class="cn">01</div>
|
||||
<h4>Круг 1 без перебиваний</h4>
|
||||
<p>4 минуты каждому без вопросов — редкость на панелях. Спикеры дают глубокие ответы, а не заготовки. Глубина = запоминаемость.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">02</div>
|
||||
<h4>Круг 2 связывает</h4>
|
||||
<p>«Что понравилось» вместо «в чём не согласен» — позитивная рамка: панель выглядит единой, а не спорящей. Зал видит созвучие индустрий.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">03</div>
|
||||
<h4>Круг 3 — артефакт</h4>
|
||||
<p>Название общего видения — то, что остаётся после сессии: в заголовках, фото, цитатах. Панель производит смысл, а не обмен мнениями.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="cn">04</div>
|
||||
<h4>Миссия 42 как рамка</h4>
|
||||
<p>«Все миссии одинаково важны» легитимирует любой ответ и любую индустрию — от ритейла до робототехники. Спикерам безопасно говорить смело.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="name">
|
||||
<div class="snum">04 / НАЗВАНИЕ ОБЩЕГО ВИДЕНИЯ</div>
|
||||
<h2>Заготовки для третьего круга</h2>
|
||||
<p class="lead">Третий круг должен закончиться названием. Если панель не сойдётся сама, модератор предлагает шаблоны — панель выбирает или дорабатывает.</p>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 01</div><div class="q">«Симбиоз» — будущее, где ИИ усиливает каждую миссию человека, не заменяя её.</div></div>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 02</div><div class="q">«Коллективный разум» — каждый умножает свой вклад через общую инфраструктуру.</div></div>
|
||||
<div class="qbox"><div class="m">ЗАГОТОВКА 03</div><div class="q">«42» — число, которое объединяет все миссии.</div></div>
|
||||
</section>
|
||||
|
||||
<section id="checklist">
|
||||
<div class="snum">05 / ЧЕК-ЛИСТ ОРГАНИЗАТОРА</div>
|
||||
<h2>Что нужно до сессии</h2>
|
||||
<ul>
|
||||
<li><b>Состав панели:</b> 5–6 спикеров из максимально разных индустрий (финансы, медицина, промышленность, образование, медиа, госсектор). Разные горизонты видения — от прагматиков до футуристов.</li>
|
||||
<li><b>Брифинг спикеров за 2–3 дня:</b> каждому объяснить три круга, тайминг 4/2 минуты и установку «загляни в самый дальний горизонт, в который можешь». Никаких презентаций.</li>
|
||||
<li><b>Модератор:</b> умеет жёстко держать тайминг и мягко вести к названию. Репетиция открытия с миссией 42.</li>
|
||||
<li><b>Тайм-кипер:</b> карточки 60/30/10 секунд, звуковой сигнал на переходах между кругами.</li>
|
||||
<li><b>Зал:</b> микрофоны для вопросов, очередь или карточки для сбора вопросов.</li>
|
||||
<li><b>Запись:</b> видео обязательно — название общего видения и лучшие цитаты круга 2 идут в посты и клипы после сессии.</li>
|
||||
<li><b>Баннер миссии:</b> слайд или растяжка с текстом миссии 42 на сцене — рамка сессии видна на любой фотографии.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section id="coop">
|
||||
<div class="snum">06 / СВЯЗЬ С КООПЕРАТИВОМ</div>
|
||||
<h2>Что это даёт 42</h2>
|
||||
<p class="lead">Панель — не только контент, но и вход в экосистему кооператива для людей с миссиями.</p>
|
||||
<ul>
|
||||
<li><b>Поток проектов:</b> спикеры и зал видят формат работы кооператива — люди с миссиями приходят в 42, регистрируют проекты, усиливают долгосрочную AUM.</li>
|
||||
<li><b>Точка входа:</b> портал cil.by упоминается в открытии и закрытии — для тех, кто захочет глубже после сессии.</li>
|
||||
<li><b>Бренд:</b> миссия 42 на сцене — фирменный приём кооператива, отличающий сессию от других панелей конференции.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Концепт панельной сессии «Общее видение» · ИИШНИЦА · кооператив «Друзья Коллективного Разума» · Минск · <span class="mono">cil.by</span><br>
|
||||
Черновик для обсуждения v0.1 · <span class="mono">simbiot-harness / hour</span> · 2026-10-09
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
out/leonid_meeting143.pptx
Normal file
BIN
out/leonid_meeting143.pptx
Normal file
Binary file not shown.
281
out/meeting143_leonid_presentation.html
Normal file
281
out/meeting143_leonid_presentation.html
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="generator" content="pandoc" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
|
||||
<title>Разговор с Леонидом — выжимка (встреча 143)</title>
|
||||
<style>
|
||||
html {
|
||||
color: #1a1a1a;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
body {
|
||||
margin: 0 auto;
|
||||
max-width: 36em;
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
padding-top: 50px;
|
||||
padding-bottom: 50px;
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-kerning: normal;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
font-size: 0.9em;
|
||||
padding: 12px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
html {
|
||||
background-color: white;
|
||||
}
|
||||
body {
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
font-size: 12pt;
|
||||
}
|
||||
p, h2, h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
h2, h3, h4 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 1em 0;
|
||||
}
|
||||
a {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
a:visited {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 1.4em;
|
||||
}
|
||||
h5, h6 {
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 1.7em;
|
||||
margin-top: 1em;
|
||||
}
|
||||
li > ol, li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
blockquote {
|
||||
margin: 1em 0 1em 1.7em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid #e6e6e6;
|
||||
color: #606060;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
hyphens: manual;
|
||||
}
|
||||
pre {
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
pre code {
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.sourceCode {
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
hr {
|
||||
background-color: #1a1a1a;
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
table {
|
||||
margin: 1em 0;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
font-variant-numeric: lining-nums tabular-nums;
|
||||
}
|
||||
table caption {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
tbody {
|
||||
margin-top: 0.5em;
|
||||
border-top: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
th {
|
||||
border-top: 1px solid #1a1a1a;
|
||||
padding: 0.25em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
td {
|
||||
padding: 0.125em 0.5em 0.25em 0.5em;
|
||||
}
|
||||
header {
|
||||
margin-bottom: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
#TOC li {
|
||||
list-style: none;
|
||||
}
|
||||
#TOC ul {
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
#TOC > ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
#TOC a:not(:hover) {
|
||||
text-decoration: none;
|
||||
}
|
||||
code{white-space: pre-wrap;}
|
||||
span.smallcaps{font-variant: small-caps;}
|
||||
div.columns{display: flex; gap: min(4vw, 1.5em);}
|
||||
div.column{flex: auto; overflow-x: auto;}
|
||||
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
|
||||
/* The extra [class] is a hack that increases specificity enough to
|
||||
override a similar rule in reveal.js */
|
||||
ul.task-list[class]{list-style: none;}
|
||||
ul.task-list li input[type="checkbox"] {
|
||||
font-size: inherit;
|
||||
width: 0.8em;
|
||||
margin: 0 0.8em 0.2em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header id="title-block-header">
|
||||
<h1 class="title">Разговор с Леонидом — выжимка (встреча 143)</h1>
|
||||
</header>
|
||||
<h1 id="разговор-алекса-с-леонидом-выжимка-и-договорённости">Разговор
|
||||
Алекса с Леонидом — выжимка и договорённости</h1>
|
||||
<p><em>Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор
|
||||
Simbiot</em></p>
|
||||
<hr />
|
||||
<h2 id="слайд-1-кто-есть-кто">Слайд 1 — Кто есть кто</h2>
|
||||
<ul>
|
||||
<li><strong>Леонид</strong> — давний знакомый студии, начинающий
|
||||
писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром
|
||||
Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём
|
||||
CUDA-моделирование графов надёжности.</li>
|
||||
<li><strong>Алекс</strong> — показал студийную инфраструктуру:
|
||||
агент-визуализатор звонков (демо в реальном времени), собственный роутер
|
||||
LM-провайдеров, RAG по всем знаниям кооператива.</li>
|
||||
<li>Демо произвело впечатление: «1-й раз такое в моей практике… польза
|
||||
очевидна».</li>
|
||||
</ul>
|
||||
<h2 id="слайд-2-демо-инфраструктуры-студии">Слайд 2 — Демо
|
||||
инфраструктуры студии</h2>
|
||||
<ul>
|
||||
<li>Агент подключается к любому звонку, рисует слайды по ходу разговора
|
||||
(задержка ~45 сек — лимит генерации).</li>
|
||||
<li>Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3
|
||||
Flash).</li>
|
||||
<li>RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний
|
||||
кооператива.</li>
|
||||
<li>Леонид получил полезный инсайт уже из демо: на его домашней
|
||||
коробочке можно крутить быструю генерацию музыки/картинок/мини-видео —
|
||||
трек за минуту.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-3-главный-контакт-пилот-мультик-по-рассказу-леонида">Слайд
|
||||
3 — Главный контакт: пилот «мультик по рассказу Леонида»</h2>
|
||||
<ul>
|
||||
<li>Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов
|
||||
<strong>финансово поддержать</strong> пилот: токены — точно,
|
||||
человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на
|
||||
эксперимент.</li>
|
||||
<li>Вход от студии: рассказ Леонида + свобода интерпретации, «никаких
|
||||
пожеланий-ограничений».</li>
|
||||
<li>Alex предложил: <strong>партнёр студии уже строит harness для
|
||||
генерации мультиков/клипов/читов</strong> — матч идеальный. Влад
|
||||
(профессиональный видеомейкер + озвучка) возьмёт видео-часть.</li>
|
||||
<li>Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем
|
||||
видео.</li>
|
||||
</ul>
|
||||
<h2
|
||||
id="слайд-4-творческий-метод-леонида-поучительно-для-продуктовой-линзы">Слайд
|
||||
4 — Творческий метод Леонида (поучительно для продуктовой линзы)</h2>
|
||||
<ul>
|
||||
<li>История с хакатоном ~2018: обучали нейросеть на сольных песнях
|
||||
Леннона/Маккартни и классифицировали битловские — «десяток песен
|
||||
правильно заклассифицировали».</li>
|
||||
<li>Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно
|
||||
маркирует «made with AI» на after.today.</li>
|
||||
<li>Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело —
|
||||
ИИ-ассистированное творчество не ново, нова честность.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-5-экономика-локального-инференса-спор-о-железе">Слайд 5 —
|
||||
Экономика локального инференса (спор о железе)</h2>
|
||||
<ul>
|
||||
<li>Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за
|
||||
«домашний интеллект»; тренд — коробки станут достоянием домов.</li>
|
||||
<li>Алекс: облачный always-on — ок для большинства задач; топовые модели
|
||||
для разработки софта всё равно выигрывают у локальных.</li>
|
||||
<li>Сошлись: гибрид — локально быстрая генерация (эскизы, музыка),
|
||||
облако — для тяжёлых продуктовых задач. У студии в планах собственный
|
||||
inference-сервер (инференс в ~6 раз быстрее).</li>
|
||||
</ul>
|
||||
<h2 id="слайд-6-обмен-тезисами-как-строить-ai-компанию">Слайд 6 — Обмен
|
||||
тезисами: как строить AI-компанию</h2>
|
||||
<ul>
|
||||
<li><strong>Леонид:</strong> «Vision — говно, execution — всё» (шутка из
|
||||
EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с
|
||||
одним промптом построят 10 одинаковых Uber’ов — вариабельность требует
|
||||
доп. информации.</li>
|
||||
<li><strong>Алекс:</strong> ядро студии — AI-ядро, постоянно
|
||||
перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно
|
||||
самый большой актив».</li>
|
||||
<li>Стратегия: строить бизнес на нейро-инфраструктуре с самого начала →
|
||||
потом переводить на автопилот; незаменим только «CEO-решатель».</li>
|
||||
<li>Леонид о студии: «я теперь понимаю, чем твоя студия отличается от
|
||||
классической — изюминка в AI-ядре».</li>
|
||||
<li>Методология Леонида для deep-tech: начинать продавать ценность сразу
|
||||
(консалтинг целевого клиента), не влюбляясь в R&D.</li>
|
||||
</ul>
|
||||
<h2 id="слайд-7-договорённости-что-фиксируем">Слайд 7 — Договорённости
|
||||
(что фиксируем)</h2>
|
||||
<ol type="1">
|
||||
<li><strong>Пилот анимации по рассказу Леонида</strong> — Leonid
|
||||
покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт
|
||||
Влад). Alex вернётся к Леониду с конкретным предложением.</li>
|
||||
<li><strong>Формат разговора продолжается</strong> — Леонид открыт к
|
||||
следующим сессиям.</li>
|
||||
<li><strong>По голосам/долям за прошлые инвестиции — Леонид
|
||||
отказался</strong> («это была шутка», «инвестирую из философских
|
||||
соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.</li>
|
||||
<li>Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с
|
||||
фидбэком.</li>
|
||||
</ol>
|
||||
<h2 id="слайд-8-что-студии-делать-дальше">Слайд 8 — Что студии делать
|
||||
дальше</h2>
|
||||
<ul>
|
||||
<li>Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета
|
||||
токенов) — Alex озвучит Леониду предметно.</li>
|
||||
<li>Подключить партнёрский harness генерации мультиков к задаче Леонида
|
||||
как первый клиентский кейс.</li>
|
||||
<li>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» —
|
||||
первое демо на живом стороннем человеке, положительный фидбэк.</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
57
out/meeting143_leonid_presentation.md
Normal file
57
out/meeting143_leonid_presentation.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Разговор Алекса с Леонидом — выжимка и договорённости
|
||||
*Встреча 143 · 2026-10-08 · ~2.5 часа · Jitsi + агент-визуализатор Simbiot*
|
||||
|
||||
---
|
||||
|
||||
## Слайд 1 — Кто есть кто
|
||||
|
||||
- **Леонид** — давний знакомый студии, начинающий писатель, инвестор в deep-tech (в т.ч. Anti-Aging Hero с физиком Петром Федичевым). Владеет DGX Spark 128GB (куплен за €5000), гоняет на нём CUDA-моделирование графов надёжности.
|
||||
- **Алекс** — показал студийную инфраструктуру: агент-визуализатор звонков (демо в реальном времени), собственный роутер LM-провайдеров, RAG по всем знаниям кооператива.
|
||||
- Демо произвело впечатление: «1-й раз такое в моей практике… польза очевидна».
|
||||
|
||||
## Слайд 2 — Демо инфраструктуры студии
|
||||
|
||||
- Агент подключается к любому звонку, рисует слайды по ходу разговора (задержка ~45 сек — лимит генерации).
|
||||
- Роутер сам выбирает LM под контекст (визуализация сейчас — GLM 5.3 Flash).
|
||||
- RAG: «он знает всё, что мы делаем» — доступ к общей базе знаний кооператива.
|
||||
- Леонид получил полезный инсайт уже из демо: на его домашней коробочке можно крутить быструю генерацию музыки/картинок/мини-видео — трек за минуту.
|
||||
|
||||
## Слайд 3 — Главный контакт: пилот «мультик по рассказу Леонида»
|
||||
|
||||
- Леонид хочет мульт-адаптации своих рассказов (10–15 мин); готов **финансово поддержать** пилот: токены — точно, человеческий труд — обсудить. Ориентир бюджета «в пределах 1000» на эксперимент.
|
||||
- Вход от студии: рассказ Леонида + свобода интерпретации, «никаких пожеланий-ограничений».
|
||||
- Alex предложил: **партнёр студии уже строит harness для генерации мультиков/клипов/читов** — матч идеальный. Влад (профессиональный видеомейкер + озвучка) возьмёт видео-часть.
|
||||
- Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео.
|
||||
|
||||
## Слайд 4 — Творческий метод Леонида (поучительно для продуктовой линзы)
|
||||
|
||||
- История с хакатоном ~2018: обучали нейросеть на сольных песнях Леннона/Маккартни и классифицировали битловские — «десяток песен правильно заклассифицировали».
|
||||
- Пишет с ИИ (своей оценки ~80% — его творчество, ~20% ИИ), честно маркирует «made with AI» на after.today.
|
||||
- Бэкграунд-тезис: «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность.
|
||||
|
||||
## Слайд 5 — Экономика локального инференса (спор о железе)
|
||||
|
||||
- Леонид: DGX Spark за ~1% стоимости дома — нормальная цена за «домашний интеллект»; тренд — коробки станут достоянием домов.
|
||||
- Алекс: облачный always-on — ок для большинства задач; топовые модели для разработки софта всё равно выигрывают у локальных.
|
||||
- Сошлись: гибрид — локально быстрая генерация (эскизы, музыка), облако — для тяжёлых продуктовых задач. У студии в планах собственный inference-сервер (инференс в ~6 раз быстрее).
|
||||
|
||||
## Слайд 6 — Обмен тезисами: как строить AI-компанию
|
||||
|
||||
- **Леонид:** «Vision — говно, execution — всё» (шутка из EPAM). Из короткого промпта не может вырасти сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует доп. информации.
|
||||
- **Алекс:** ядро студии — AI-ядро, постоянно перестраивающее паттерны (64 сейчас) при смене контекста — «долгосрочно самый большой актив».
|
||||
- Стратегия: строить бизнес на нейро-инфраструктуре с самого начала → потом переводить на автопилот; незаменим только «CEO-решатель».
|
||||
- Леонид о студии: «я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре».
|
||||
- Методология Леонида для deep-tech: начинать продавать ценность сразу (консалтинг целевого клиента), не влюбляясь в R&D.
|
||||
|
||||
## Слайд 7 — Договорённости (что фиксируем)
|
||||
|
||||
1. **Пилот анимации по рассказу Леонида** — Leonid покрывает токены (ориентир до 1000), студия делает (аудио → видео, ведёт Влад). Alex вернётся к Леониду с конкретным предложением.
|
||||
2. **Формат разговора продолжается** — Леонид открыт к следующим сессиям.
|
||||
3. **По голосам/долям за прошлые инвестиции — Леонид отказался** («это была шутка», «инвестирую из философских соображений, не создавайте ожиданий»). Вопрос закрыт, давления нет.
|
||||
4. Леонид ещё посмотрит презентацию студии (35-минутную) и вернётся с фидбэком.
|
||||
|
||||
## Слайд 8 — Что студии делать дальше
|
||||
|
||||
- Подготовить оффер по мульт-пилоту (объём, этап аудиоспектакля, смета токенов) — Alex озвучит Леониду предметно.
|
||||
- Подключить партнёрский harness генерации мультиков к задаче Леонида как первый клиентский кейс.
|
||||
- Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке» — первое демо на живом стороннем человеке, положительный фидбэк.
|
||||
436
out/meeting143_leonid_slides.html
Normal file
436
out/meeting143_leonid_slides.html
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Встреча 143 · Алекс × Леонид — выжимка и договорённости</title>
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0a0c12;
|
||||
--bg2:#0e1120;
|
||||
--ink:#f0eeff;
|
||||
--mut:#9a97b8;
|
||||
--dim:#6f6c8f;
|
||||
--vio:#8b5cf6;
|
||||
--vio2:#7c6cf5;
|
||||
--cyan:#5eead4;
|
||||
--amber:#fbbf24;
|
||||
--card:rgba(255,255,255,.035);
|
||||
--line:rgba(139,92,246,.28);
|
||||
--serif:Georgia,"Times New Roman",serif;
|
||||
--sans:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
--mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
background:var(--bg);
|
||||
color:var(--ink);
|
||||
font-family:var(--sans);
|
||||
overflow:hidden;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
text-rendering:optimizeLegibility;
|
||||
}
|
||||
/* ---------- ambient backdrop ---------- */
|
||||
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
|
||||
.bg::before{
|
||||
content:"";position:absolute;inset:0;
|
||||
background:
|
||||
radial-gradient(60vw 60vw at 12% -10%, rgba(124,108,245,.20), transparent 62%),
|
||||
radial-gradient(55vw 55vw at 105% 112%, rgba(94,234,212,.09), transparent 60%),
|
||||
radial-gradient(70vw 40vw at 50% 120%, rgba(139,92,246,.10), transparent 65%),
|
||||
linear-gradient(180deg,var(--bg) 0%,var(--bg2) 100%);
|
||||
}
|
||||
.bg::after{ /* film grain */
|
||||
content:"";position:absolute;inset:0;opacity:.05;mix-blend-mode:overlay;
|
||||
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/></filter><rect width='160' height='160' filter='url(%23n)' opacity='0.6'/></svg>");
|
||||
}
|
||||
.vignette{position:fixed;inset:0;z-index:1;pointer-events:none;
|
||||
background:radial-gradient(120vw 90vh at 50% 50%, transparent 55%, rgba(0,0,0,.55) 100%)}
|
||||
|
||||
/* ---------- deck ---------- */
|
||||
.deck{position:relative;z-index:2;height:100%}
|
||||
.slide{
|
||||
position:absolute;inset:0;
|
||||
display:flex;flex-direction:column;justify-content:center;
|
||||
padding:9vh 8vw 12vh;
|
||||
opacity:0;transform:translateY(26px) scale(.985);
|
||||
transition:opacity .55s ease,transform .55s cubic-bezier(.22,.61,.36,1);
|
||||
pointer-events:none;visibility:hidden;
|
||||
}
|
||||
.slide.active{opacity:1;transform:none;pointer-events:auto;visibility:visible}
|
||||
.slide.prev{transform:translateY(-26px) scale(.985)}
|
||||
|
||||
.accent{position:absolute;top:0;left:0;right:0;height:6px;
|
||||
background:linear-gradient(90deg,var(--vio) 0%,#a78bfa 38%,var(--cyan) 100%);
|
||||
box-shadow:0 0 24px rgba(139,92,246,.55)}
|
||||
.ghost{
|
||||
position:absolute;right:4vw;bottom:2vh;z-index:-1;
|
||||
font-weight:900;font-size:clamp(160px,30vw,420px);line-height:1;
|
||||
color:transparent;-webkit-text-stroke:1.5px rgba(139,92,246,.13);
|
||||
letter-spacing:-.04em;user-select:none}
|
||||
|
||||
.kicker{
|
||||
display:inline-flex;align-items:center;gap:.6em;
|
||||
font-size:clamp(11px,1.15vw,14px);font-weight:700;letter-spacing:.24em;
|
||||
text-transform:uppercase;color:var(--vio2);margin-bottom:2.4vh;
|
||||
}
|
||||
.kicker::before{content:"";width:34px;height:2px;background:var(--vio);box-shadow:0 0 12px var(--vio)}
|
||||
h1{font-size:clamp(38px,6.6vw,86px);font-weight:900;letter-spacing:-.03em;line-height:1.04}
|
||||
h1 em{font-style:normal;background:linear-gradient(92deg,#a78bfa,var(--vio2) 55%,var(--cyan));
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
h2{font-size:clamp(28px,4vw,52px);font-weight:800;letter-spacing:-.025em;line-height:1.08;margin-bottom:3.6vh}
|
||||
h2 .alt{color:var(--vio2)}
|
||||
.sub{font-size:clamp(15px,1.6vw,21px);color:var(--mut);max-width:34em;line-height:1.55;margin-top:3vh}
|
||||
|
||||
.chips{display:flex;flex-wrap:wrap;gap:.7em;margin-top:4.5vh}
|
||||
.chip{
|
||||
font-family:var(--mono);font-size:clamp(11px,1.05vw,13px);color:var(--mut);
|
||||
padding:.55em 1.1em;border:1px solid var(--line);border-radius:999px;
|
||||
background:rgba(139,92,246,.07)}
|
||||
.chip b{color:var(--ink);font-weight:600}
|
||||
|
||||
/* ---------- content blocks ---------- */
|
||||
.grid{display:grid;gap:1.5vh 1.6vw}
|
||||
.g2{grid-template-columns:repeat(2,1fr)}
|
||||
.g3{grid-template-columns:repeat(3,1fr)}
|
||||
@media(max-width:860px){.g2,.g3{grid-template-columns:1fr}}
|
||||
|
||||
.card{
|
||||
background:var(--card);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:18px;padding:2.6vh 1.6vw;position:relative;overflow:hidden;
|
||||
backdrop-filter:blur(6px);
|
||||
transition:border-color .3s,transform .3s;
|
||||
}
|
||||
.card:hover{border-color:var(--line);transform:translateY(-2px)}
|
||||
.card::before{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;
|
||||
background:linear-gradient(180deg,var(--vio),transparent 85%);opacity:.85}
|
||||
.card h3{font-size:clamp(15px,1.45vw,19px);font-weight:700;margin-bottom:1vh;letter-spacing:-.01em}
|
||||
.card p,.card li{font-size:clamp(13px,1.28vw,17px);color:var(--mut);line-height:1.62}
|
||||
.card p b,.card li b{color:var(--ink);font-weight:600}
|
||||
.tag{display:inline-block;font-family:var(--mono);font-size:clamp(10px,.95vw,12px);
|
||||
color:var(--cyan);border:1px solid rgba(94,234,212,.3);border-radius:6px;
|
||||
padding:.15em .6em;margin-bottom:1.2vh;letter-spacing:.06em}
|
||||
.tag.v{color:#c4b5fd;border-color:rgba(139,92,246,.4)}
|
||||
.tag.a{color:var(--amber);border-color:rgba(251,191,36,.35)}
|
||||
|
||||
ul.list{list-style:none;display:grid;gap:1.7vh}
|
||||
ul.list li{
|
||||
display:flex;gap:1em;align-items:flex-start;
|
||||
font-size:clamp(14px,1.5vw,19px);color:var(--mut);line-height:1.6;
|
||||
}
|
||||
ul.list li::before{
|
||||
content:"▸";color:var(--vio);font-size:1.05em;line-height:1.5;
|
||||
text-shadow:0 0 14px rgba(139,92,246,.8);flex:0 0 auto}
|
||||
ul.list b{color:var(--ink);font-weight:650}
|
||||
ul.list .hl{color:var(--cyan)}
|
||||
|
||||
.quote{
|
||||
margin-top:3.2vh;padding:2.4vh 2vw;border-left:3px solid var(--vio);
|
||||
background:linear-gradient(90deg,rgba(139,92,246,.12),transparent 72%);
|
||||
border-radius:0 14px 14px 0;
|
||||
}
|
||||
.quote p{font-family:var(--serif);font-style:italic;
|
||||
font-size:clamp(17px,2.1vw,27px);line-height:1.45;color:#e4e0ff}
|
||||
.quote cite{display:block;margin-top:1.2vh;font-style:normal;font-family:var(--mono);
|
||||
font-size:clamp(10px,1vw,13px);color:var(--dim);letter-spacing:.12em;text-transform:uppercase}
|
||||
|
||||
.num{display:flex;gap:1.1em;align-items:flex-start;padding:1.9vh 0;
|
||||
border-bottom:1px dashed rgba(255,255,255,.09)}
|
||||
.num:last-child{border-bottom:none}
|
||||
.num i{
|
||||
font-style:normal;font-family:var(--mono);flex:0 0 auto;
|
||||
width:2.1em;height:2.1em;display:grid;place-items:center;border-radius:10px;
|
||||
background:rgba(139,92,246,.16);border:1px solid var(--line);
|
||||
color:#c4b5fd;font-size:clamp(12px,1.15vw,15px);font-weight:700}
|
||||
.num h3{font-size:clamp(15px,1.5vw,19px);font-weight:700;margin-bottom:.5vh}
|
||||
.num p{font-size:clamp(13px,1.3vw,16px);color:var(--mut);line-height:1.6}
|
||||
.num b{color:var(--ink);font-weight:600}
|
||||
|
||||
.vs{display:grid;grid-template-columns:1fr auto 1fr;gap:1.2vw;align-items:stretch}
|
||||
@media(max-width:860px){.vs{grid-template-columns:1fr}.vs .vsx{display:none}}
|
||||
.vs .card{height:100%}
|
||||
.vs .who{font-family:var(--mono);font-size:clamp(10px,.95vw,12px);letter-spacing:.14em;
|
||||
text-transform:uppercase;color:var(--vio2);margin-bottom:1.2vh}
|
||||
.vsx{display:grid;place-items:center;color:var(--dim);font-size:clamp(18px,2vw,26px)}
|
||||
|
||||
.big{font-size:clamp(30px,4.6vw,64px);font-weight:900;letter-spacing:-.03em;line-height:1.1}
|
||||
.big em{font-style:normal;color:var(--vio2)}
|
||||
|
||||
/* ---------- chrome ---------- */
|
||||
.progress{position:fixed;left:0;bottom:0;height:3px;z-index:10;width:0;
|
||||
background:linear-gradient(90deg,var(--vio),#a78bfa,var(--cyan));
|
||||
box-shadow:0 0 14px rgba(139,92,246,.8);transition:width .45s ease}
|
||||
.hud{position:fixed;right:2.2vw;bottom:3vh;z-index:10;display:flex;gap:.6em;align-items:center}
|
||||
.hud .count{font-family:var(--mono);font-size:12px;color:var(--dim);letter-spacing:.1em;margin-right:.8em}
|
||||
.hud .count b{color:var(--mut);font-weight:600}
|
||||
.hud button{
|
||||
width:40px;height:40px;border-radius:12px;cursor:pointer;
|
||||
border:1px solid var(--line);background:rgba(139,92,246,.1);color:#c4b5fd;
|
||||
font-size:16px;display:grid;place-items:center;transition:.25s}
|
||||
.hud button:hover{background:rgba(139,92,246,.25);color:#fff;transform:translateY(-1px)}
|
||||
.hint{position:fixed;left:2.2vw;bottom:3vh;z-index:10;
|
||||
font-family:var(--mono);font-size:11px;color:var(--dim);letter-spacing:.08em;opacity:.8}
|
||||
.brand{position:fixed;top:2.6vh;right:2.2vw;z-index:10;
|
||||
font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--dim);text-transform:uppercase}
|
||||
.brand b{color:var(--vio2)}
|
||||
.dots{position:fixed;right:1.4vw;top:50%;transform:translateY(-50%);z-index:10;
|
||||
display:flex;flex-direction:column;gap:.55em}
|
||||
.dots i{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.16);cursor:pointer;transition:.3s}
|
||||
.dots i.on{background:var(--vio);box-shadow:0 0 10px var(--vio);transform:scale(1.35)}
|
||||
@media(max-width:700px){.dots{display:none}.hint{display:none}}
|
||||
|
||||
.end .sig{font-family:var(--mono);font-size:clamp(10px,1vw,13px);color:var(--dim);
|
||||
letter-spacing:.18em;text-transform:uppercase;margin-top:5vh}
|
||||
|
||||
/* ---------- print ---------- */
|
||||
@media print{
|
||||
body{overflow:visible;background:#0a0c12}
|
||||
.bg,.vignette,.hud,.hint,.dots,.progress,.brand{display:none!important}
|
||||
.deck{height:auto}
|
||||
.slide{position:relative;inset:auto;opacity:1!important;transform:none!important;
|
||||
visibility:visible!important;pointer-events:auto;height:100vh;page-break-after:always}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div><div class="vignette"></div>
|
||||
<div class="brand">Simbiot · <b>Кооператив 42</b></div>
|
||||
|
||||
<div class="deck" id="deck">
|
||||
|
||||
<!-- 01 · TITLE -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">143</div>
|
||||
<div class="kicker">Встреча 143 · 08.10.2026 · Jitsi + агент-визуализатор Simbiot</div>
|
||||
<h1>Разговор<br>Алекса <em>× Леонида</em></h1>
|
||||
<p class="sub">Выжимка двухсполовинойчасовой встречи: инфраструктура студии, пилот «мультик по рассказу», экономика локального инференса — и договорённости, которые фиксируем.</p>
|
||||
<div class="chips">
|
||||
<span class="chip"><b>~2.5</b> часа разговора</span>
|
||||
<span class="chip"><b>8</b> тем</span>
|
||||
<span class="chip"><b>4</b> договорённости</span>
|
||||
<span class="chip">слайды рисовал агент <b>live</b></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 02 · WHO -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">01</div>
|
||||
<div class="kicker">Кто есть кто</div>
|
||||
<h2>Два мира на одном звонке</h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="who tag v">Леонид</div>
|
||||
<h3>Писатель · инвестор в deep-tech</h3>
|
||||
<p>Давний знакомый студии. Инвестирует в deep-tech, в том числе <b>Anti-Aging Hero</b> с физиком Петром Федичевым. Дома — <b>DGX Spark 128GB</b> (€5000), на которой он гоняет CUDA-моделирование графов надёжности.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag">Алекс · студия</div>
|
||||
<h3>Живая AI-инфраструктура</h3>
|
||||
<p>Показал, чем студия отличается от классической: <b>агент-визуализатор звонков</b> (демо в реальном времени), собственный <b>роутер LM-провайдеров</b> и <b>RAG</b> по всем знаниям кооператива.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>«Первый раз такое в моей практике… польза очевидна»</p>
|
||||
<cite>Леонид — после демо агент-визуализатора</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 03 · DEMO -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">02</div>
|
||||
<div class="kicker">Демо инфраструктуры</div>
|
||||
<h2>Что увидел Леонид <span class="alt">в прямом эфире</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Агент подключается к любому звонку</b> и рисует слайды по ходу разговора — задержка ~45 секунд (лимит генерации)</span></li>
|
||||
<li><span><b>Роутер сам выбирает модель под контекст</b> — визуализация идёт на GLM 5.3 Flash</span></li>
|
||||
<li><span><b>RAG по базе кооператива:</b> «он знает всё, что мы делаем» — агент отвечает со знанием всей истории студии</span></li>
|
||||
<li><span>Инсайт Леонида из демо: <span class="hl">на его домашней коробочке можно крутить быструю генерацию</span> — музыку, картинки, мини-видео. Трек за минуту</span></li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- 04 · PILOT -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">03</div>
|
||||
<div class="kicker">Главный контакт встречи</div>
|
||||
<h2>Пилот: <span class="alt">мультик по рассказу Леонида</span></h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="tag a">Что хочет Леонид</div>
|
||||
<p><b>Мульт-адаптация своих рассказов</b> (10–15 минут). Готов финансово поддержать пилот: <b>токены — точно</b>, человеческий труд — обсудить. Ориентир бюджета — <b>«в пределах 1000»</b> на эксперимент.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag v">Что входит студия</div>
|
||||
<p>Рассказ Леонида + <b>свобода интерпретации</b> («никаких пожеланий-ограничений»). Со стороны студии — партнёр уже строит <b>harness генерации мультиков</b>, видео-часть ведёт <b>Влад</b> (профессиональный видеомейкер + озвучка). Матч идеальный.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>Первый шаг — аудиоспектакль: дешевле и предсказуемее. Затем видео.</p>
|
||||
<cite>Дорожная карта пилота</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 05 · METHOD -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">04</div>
|
||||
<div class="kicker">Творческий метод Леонида</div>
|
||||
<h2>Поучительно для <span class="alt">продуктовой линзы</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Хакатон ~2018:</b> обучали нейросеть на сольных песнях Леннона и Маккартни, затем классифицировали битловские — «десяток песен правильно заклассифицировали»</span></li>
|
||||
<li><span><b>Пишет с ИИ:</b> по собственной оценке ~80% — его творчество, ~20% — ИИ. Честно маркирует <span class="hl">«made with AI»</span> на after.today</span></li>
|
||||
<li><span><b>Бэкграунд-тезис:</b> «литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново. <b>Нова честность.</b></span></li>
|
||||
</ul>
|
||||
<div class="quote">
|
||||
<p>ИИ-ассистированное творчество не ново — нова честность.</p>
|
||||
<cite>Леонид</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 06 · HARDWARE -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">05</div>
|
||||
<div class="kicker">Экономика локального инференса</div>
|
||||
<h2>Спор о железе: <span class="alt">дом против облака</span></h2>
|
||||
<div class="vs">
|
||||
<div class="card">
|
||||
<div class="who">Позиция Леонида</div>
|
||||
<p><b>DGX Spark за ~1% стоимости дома</b> — нормальная цена за «домашний интеллект». Тренд: коробки станут достоянием домов.</p>
|
||||
</div>
|
||||
<div class="vsx">⚡</div>
|
||||
<div class="card">
|
||||
<div class="who">Позиция Алекса</div>
|
||||
<p><b>Облачный always-on</b> — ок для большинства задач. Топовые модели для разработки софта всё равно выигрывают у локальных.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>Сошлись на гибриде: локально — быстрая генерация (эскизы, музыка), облако — тяжёлые продуктовые задачи. У студии в планах собственный inference-сервер — в ~6 раз быстрее.</p>
|
||||
<cite>Консенсус встречи</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 07 · AI COMPANY -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">06</div>
|
||||
<div class="kicker">Обмен тезисами</div>
|
||||
<h2>Как строить <span class="alt">AI-компанию</span></h2>
|
||||
<div class="grid g2">
|
||||
<div class="card">
|
||||
<div class="tag">Тезис Леонида</div>
|
||||
<p><b>«Vision — говно, execution — всё»</b> (шутка из EPAM). Из короткого промпта не вырастает сложная система: 10 людей с одним промптом построят 10 одинаковых Uber'ов — вариабельность требует дополнительной информации.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="tag v">Тезис Алекса</div>
|
||||
<p>Ядро студии — <b>AI-ядро, постоянно перестраивающее паттерны</b> (64 сейчас) при смене контекста. «Долгосрочно — самый большой актив». Строить бизнес на нейро-инфраструктуре с самого начала → затем автопилот. Незаменим только <b>«CEO-решатель»</b>.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quote">
|
||||
<p>«Я теперь понимаю, чем твоя студия отличается от классической — изюминка в AI-ядре»</p>
|
||||
<cite>Леонид · метод deep-tech: продавать ценность сразу, не влюбляясь в R&D</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 08 · DEALS -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">07</div>
|
||||
<div class="kicker">Итоги встречи</div>
|
||||
<h2>Договорённости — <span class="alt">что фиксируем</span></h2>
|
||||
<div class="num"><i>1</i><div>
|
||||
<h3>Пилот анимации по рассказу Леонида</h3>
|
||||
<p>Леонид покрывает токены (ориентир до <b>1000</b>), студия делает: аудио → видео, ведёт Влад. Алекс вернётся с <b>конкретным предложением</b>.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>2</i><div>
|
||||
<h3>Формат разговора продолжается</h3>
|
||||
<p>Леонид открыт к следующим сессиям со студией.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>3</i><div>
|
||||
<h3>Голоса и доли за прошлые инвестиции — вопрос закрыт</h3>
|
||||
<p>Леонид отказался: «это была шутка», «инвестирую из философских соображений, не создавайте ожиданий». Давления нет.</p>
|
||||
</div></div>
|
||||
<div class="num"><i>4</i><div>
|
||||
<h3>Следующий шаг Леонида</h3>
|
||||
<p>Посмотрит 35-минутную презентацию студии и вернётся с фидбэком.</p>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
<!-- 09 · NEXT -->
|
||||
<section class="slide">
|
||||
<div class="accent"></div><div class="ghost">08</div>
|
||||
<div class="kicker">Action items</div>
|
||||
<h2>Что студии делать <span class="alt">дальше</span></h2>
|
||||
<ul class="list">
|
||||
<li><span><b>Подготовить оффер по мульт-пилоту</b> — объём, этап аудиоспектакля, смета токенов. Алекс озвучит Леониду предметно.</span></li>
|
||||
<li><span><b>Подключить партнёрский harness генерации мультиков</b> к задаче Леонида — как первый клиентский кейс.</span></li>
|
||||
<li><span><b>Зафиксировать кейс «агент-визуализатор на реальном внешнем звонке»</b> — первое демо на живом стороннем человеке, положительный фидбэк.</span></li>
|
||||
</ul>
|
||||
<div class="quote">
|
||||
<p>От живого демо на звонке — к первому платному пилоту по генерации контента.</p>
|
||||
<cite>Смысл встречи 143</cite>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 10 · END -->
|
||||
<section class="slide end">
|
||||
<div class="accent"></div><div class="ghost">42</div>
|
||||
<div class="kicker">Встреча 143 · 08.10.2026</div>
|
||||
<div class="big">«Изюминка — <em>в AI-ядре»</em></div>
|
||||
<p class="sub">Simbiot · Кооператив 42 · агент-визуализатор, LM-роутер, RAG по знаниям студии. Презентация собрана из выжимки разговора — тем же агентом, что рисовал слайды live.</p>
|
||||
<div class="sig">← → навигация · F — во весь экран · P — печать/PDF</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dots" id="dots"></div>
|
||||
<div class="progress" id="prog"></div>
|
||||
<div class="hud">
|
||||
<span class="count" id="count"></span>
|
||||
<button id="prev" title="Назад (←)">←</button>
|
||||
<button id="next" title="Вперёд (→)">→</button>
|
||||
</div>
|
||||
<div class="hint">← → · свайп · F — фулскрин</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var slides=[].slice.call(document.querySelectorAll('.slide'));
|
||||
var dots=document.getElementById('dots'),prog=document.getElementById('prog'),cnt=document.getElementById('count');
|
||||
var i=0,n=slides.length;
|
||||
slides.forEach(function(_,k){var d=document.createElement('i');d.onclick=function(){go(k)};dots.appendChild(d);});
|
||||
function pad(x){return (x<10?'0':'')+x}
|
||||
function go(k){
|
||||
k=Math.max(0,Math.min(n-1,k));
|
||||
if(k===i&&slides[i].classList.contains('active'))return;
|
||||
slides.forEach(function(s,j){
|
||||
s.classList.toggle('active',j===k);
|
||||
s.classList.toggle('prev',j<k);
|
||||
});
|
||||
i=k;
|
||||
prog.style.width=((i+1)/n*100)+'%';
|
||||
cnt.innerHTML='<b>'+pad(i+1)+'</b> / '+pad(n);
|
||||
[].forEach.call(dots.children,function(d,j){d.classList.toggle('on',j===i)});
|
||||
if(history.replaceState)history.replaceState(null,'','#'+(i+1));
|
||||
}
|
||||
document.getElementById('next').onclick=function(){go(i+1)};
|
||||
document.getElementById('prev').onclick=function(){go(i-1)};
|
||||
addEventListener('keydown',function(e){
|
||||
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '){e.preventDefault();go(i+1)}
|
||||
else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(i-1)}
|
||||
else if(e.key==='Home'){go(0)}
|
||||
else if(e.key==='End'){go(n-1)}
|
||||
else if(e.key==='f'||e.key==='F'||e.key==='а'||e.key==='А'){
|
||||
if(document.fullscreenElement){document.exitFullscreen&&document.exitFullscreen()}
|
||||
else{document.documentElement.requestFullscreen&&document.documentElement.requestFullscreen()}
|
||||
}
|
||||
});
|
||||
var tx=null,ty=null;
|
||||
addEventListener('touchstart',function(e){tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
|
||||
addEventListener('touchend',function(e){
|
||||
if(tx===null)return;
|
||||
var dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty;
|
||||
if(Math.abs(dx)>60&&Math.abs(dx)>Math.abs(dy)){go(dx<0?i+1:i-1)}
|
||||
tx=null;
|
||||
},{passive:true});
|
||||
var h=parseInt((location.hash||'').replace('#',''),10);
|
||||
go(isNaN(h)?0:h-1);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
out/simbiot_pitch.pptx
Normal file
BIN
out/simbiot_pitch.pptx
Normal file
Binary file not shown.
80
out/static-files-index.html
Normal file
80
out/static-files-index.html
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simbiot — индекс статических файлов</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,sans-serif;background:#0e1116;color:#e6e9ef;margin:0;padding:40px 20px}
|
||||
h1{font-size:26px} h2{margin-top:36px;font-size:19px;color:#8ab4ff;border-bottom:1px solid #2a3140;padding-bottom:6px}
|
||||
table{width:100%;max-width:1000px;border-collapse:collapse;font-size:14px}
|
||||
td{padding:8px 10px;border-bottom:1px solid #1e2430;vertical-align:top}
|
||||
a{color:#7ec9ff;text-decoration:none} a:hover{text-decoration:underline}
|
||||
.size{color:#8a93a5;white-space:nowrap}
|
||||
.note{color:#aab4c5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📁 Индекс статических файлов кооператива 42 / Simbiot</h1>
|
||||
<p class="note">Все HTML/PNG/PDF-файлы репозитория, сгруппированные по категориям. 31 файл.</p>
|
||||
|
||||
<h2>🌍 Лендинги и презентации</h2>
|
||||
<table>
|
||||
<tr><td><a href="../docs/simbiot-landing-v2.html">docs/simbiot-landing-v2.html</a></td><td class="note">Лендинг Simbiot v2</td><td class="size">33 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot-landing-v2-live.html">docs/simbiot-landing-v2-live.html</a></td><td class="note">Лендинг v2 (live-версия)</td><td class="size">30 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot-landing-dashboard.html">docs/simbiot-landing-dashboard.html</a></td><td class="note">Лендинг + дашборд</td><td class="size">20 КБ</td></tr>
|
||||
<tr><td><a href="../docs/simbiot_presale_landing.html">docs/simbiot_presale_landing.html</a></td><td class="note">Пресейл-лендинг</td><td class="size">17 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/landing-simbiot.html">artifacts/landing-simbiot.html</a></td><td class="note">Лендинг (артefакт)</td><td class="size">11 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/landing-simbiot-opencode.html">artifacts/landing-simbiot-opencode.html</a></td><td class="note">Лендинг (OpenCode)</td><td class="size">20 КБ</td></tr>
|
||||
<tr><td><a href="../simbiot-presentation.html">simbiot-presentation.html</a></td><td class="note">Презентация Simbiot</td><td class="size">22 КБ</td></tr>
|
||||
<tr><td><a href="../architecture.html">architecture.html</a></td><td class="note">Архитектура системы</td><td class="size">40 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>👤 Порталы и UI-клиенты</h2>
|
||||
<table>
|
||||
<tr><td><a href="../out/cil-by-compact-landing.html">out/cil-by-compact-landing.html</a></td><td class="note">Портал CIL by Compact</td><td class="size">23 КБ</td></tr>
|
||||
<tr><td><a href="../out/ui-cil-by-compact-landing.html">out/ui-cil-by-compact-landing.html</a></td><td class="note">UI-версия портала CIL</td><td class="size">22 КБ</td></tr>
|
||||
<tr><td><a href="../out/simbiot_ui_presale.html">out/simbiot_ui_presale.html</a></td><td class="note">Пресейл UI</td><td class="size">12 КБ</td></tr>
|
||||
<tr><td><a href="../harness/roy.html">harness/roy.html</a></td><td class="note">Портал Roy</td><td class="size">3 КБ</td></tr>
|
||||
<tr><td><a href="../harness/dashboard.html">harness/dashboard.html</a></td><td class="note">Дашборд харнеса</td><td class="size">40 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🗓 Встречи и материалы к переговорам</h2>
|
||||
<table>
|
||||
<tr><td><a href="../out/meeting143_leonid_presentation.html">out/meeting143_leonid_presentation.html</a></td><td class="note">Встреча 143 — презентация для Леонида</td><td class="size">13 КБ</td></tr>
|
||||
<tr><td><a href="../out/meeting143_leonid_slides.html">out/meeting143_leonid_slides.html</a></td><td class="note">Встреча 143 — слайды</td><td class="size">25 КБ</td></tr>
|
||||
<tr><td><a href="../docs/yulia-call-136-proposal.html">docs/yulia-call-136-proposal.html</a></td><td class="note">Звонок 136 — предложение Юлии</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../docs/yulia-call-136-recap.html">docs/yulia-call-136-recap.html</a></td><td class="note">Звонок 136 — рекап</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../docs/nadezhda-call-recap.html">docs/nadezhda-call-recap.html</a></td><td class="note">Звонок с Надеждой — рекап</td><td class="size">10 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>📄 Документы и договоры</h2>
|
||||
<table>
|
||||
<tr><td><a href="../Studio_Participation_Agreement_2026-09.pdf">Studio_Participation_Agreement_2026-09.pdf</a></td><td class="note">Соглашение об участии в студии (PDF)</td><td class="size">17 КБ</td></tr>
|
||||
<tr><td><a href="../lab42-conditions.html">lab42-conditions.html</a></td><td class="note">Условия Lab42</td><td class="size">15 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/studio-conditions-2026-10.html">artifacts/studio-conditions-2026-10.html</a></td><td class="note">Условия студии 2026-10</td><td class="size">6 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/rental-x-simbiot-agreement.html">artifacts/rental-x-simbiot-agreement.html</a></td><td class="note">Соглашение Rental×Simbiot</td><td class="size">16 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/rental-x-simbiot-side-letter-15k.html">artifacts/rental-x-simbiot-side-letter-15k.html</a></td><td class="note">Сайд-леттер Rental×Simbiot 15k</td><td class="size">10 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🛠 Продуктовые планы и гайды</h2>
|
||||
<table>
|
||||
<tr><td><a href="../docs/ai-mascots-product-plan.html">docs/ai-mascots-product-plan.html</a></td><td class="note">Продуктовый план AI-маскотов</td><td class="size">16 КБ</td></tr>
|
||||
<tr><td><a href="../docs/fpga-neuralnet-guide.html">docs/fpga-neuralnet-guide.html</a></td><td class="note">Гайд: нейросети на FPGA</td><td class="size">9 КБ</td></tr>
|
||||
<tr><td><a href="../docs/self-diagnostics.html">docs/self-diagnostics.html</a></td><td class="note">Самодиагностика</td><td class="size">8 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🎨 Прототипы</h2>
|
||||
<table>
|
||||
<tr><td><a href="../artifacts/shkor-beauty-prototype.html">artifacts/shkor-beauty-prototype.html</a></td><td class="note">Прототип Shkor Beauty</td><td class="size">25 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>🖼 Изображения</h2>
|
||||
<table>
|
||||
<tr><td><a href="../artifacts/imagegen-e2e-20261003.png">artifacts/imagegen-e2e-20261003.png</a></td><td class="note">E2E-тест генерации изображений</td><td class="size">977 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b.png">artifacts/dashboard-channels-t-d8112b.png</a></td><td class="note">Скриншот дашборда (каналы)</td><td class="size">138 КБ</td></tr>
|
||||
<tr><td><a href="../artifacts/dashboard-channels-t-d8112b-full.png">artifacts/dashboard-channels-t-d8112b-full.png</a></td><td class="note">Скриншот дашборда (полный)</td><td class="size">145 КБ</td></tr>
|
||||
<tr><td><a href="../out/9router-gemini-test.png">out/9router-gemini-test.png</a></td><td class="note">Тест 9router/Gemini</td><td class="size">1 КБ</td></tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
133
scripts/make_pptx_261008.py
Normal file
133
scripts/make_pptx_261008.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Генерация PPTX: (1) разговор с Леонидом, (2) питч Simbiot (Seedance)."""
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
|
||||
BG = RGBColor(0x0A, 0x0C, 0x12)
|
||||
ACCENT = RGBColor(0x8B, 0x5C, 0xF6)
|
||||
ACCENT2 = RGBColor(0x7C, 0x6C, 0xF5)
|
||||
TEXT = RGBColor(0xF0, 0xEE, 0xFF)
|
||||
MUT = RGBColor(0x9A, 0x97, 0xB8)
|
||||
|
||||
def add_slide(prs, title, bullets, kicker=None):
|
||||
s = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
s.background.fill.solid(); s.background.fill.fore_color.rgb = BG
|
||||
w, h = prs.slide_width, prs.slide_height
|
||||
# accent bar
|
||||
bar = s.shapes.add_shape(1, Emu(0), Emu(0), Emu(w), Inches(0.09))
|
||||
bar.fill.solid(); bar.fill.fore_color.rgb = ACCENT; bar.line.fill.background()
|
||||
if kicker:
|
||||
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.45), Inches(11.8), Inches(0.4))
|
||||
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = kicker.upper()
|
||||
r.font.size = Pt(12); r.font.color.rgb = ACCENT2; r.font.bold = True
|
||||
tb = s.shapes.add_textbox(Inches(0.7), Inches(0.85), Inches(12.0), Inches(1.0))
|
||||
tf = tb.text_frame; p = tf.paragraphs[0]; r = p.add_run(); r.text = title
|
||||
r.font.size = Pt(30); r.font.color.rgb = TEXT; r.font.bold = True
|
||||
tb = s.shapes.add_textbox(Inches(0.75), Inches(1.85), Inches(11.9), Inches(4.9))
|
||||
tf = tb.text_frame; tf.word_wrap = True
|
||||
first = True
|
||||
for b in bullets:
|
||||
lvl = 0
|
||||
if isinstance(b, tuple): b, lvl = b
|
||||
p = tf.paragraphs[0] if first else tf.add_paragraph(); first = False
|
||||
r = p.add_run(); r.text = ("• " if lvl == 0 else "– ") + b
|
||||
r.font.size = Pt(16 if lvl == 0 else 14); r.font.color.rgb = TEXT if lvl == 0 else MUT
|
||||
p.space_after = Pt(8)
|
||||
return s
|
||||
|
||||
prs = Presentation(); prs.slide_width = Inches(13.333); prs.slide_height = Inches(7.5)
|
||||
|
||||
K = "Встреча 143 · 2026-10-08"
|
||||
add_slide(prs, "Разговор Алекса с Леонидом", [
|
||||
"Выжимка разговора и договорённости · ~2.5 часа",
|
||||
"Jitsi + агент-визуализатор Simbiot (слайды рисуются в реальном времени)",
|
||||
"Кооператив 42 / студия Simbiot",
|
||||
], kicker=K)
|
||||
add_slide(prs, "Кто есть кто", [
|
||||
"Леонид — давний знакомый студии, писатель, инвестор в deep-tech (Anti-Aging Hero с физиком Петром Федичевым)",
|
||||
"DGX Spark 128GB (€5000) — дома гоняет CUDA-моделирование графов надёжности",
|
||||
"Алекс — инфраструктура студии: агент-визуализатор, собственный роутер LM, RAG по знаниям кооператива",
|
||||
("«1-й раз такое в моей практике… польза очевидна» — Леонид о демо", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Демо инфраструктуры студии", [
|
||||
"Агент подключается к любому звонку и рисует слайды по ходу разговора (~45 сек на слайд)",
|
||||
"Роутер сам выбирает LM под контекст (визуализация — GLM 5.3 Flash)",
|
||||
"RAG: «он знает всё, что мы делаем» — общая база знаний кооператива",
|
||||
("Инсайт Леонида: домашняя коробочка → быстрая генерация музыки/картинок/мини-видео", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Пилот: мультик по рассказу Леонида", [
|
||||
"Мульт-адаптация рассказов (10–15 мин); Леонид готов финансово поддержать пилот",
|
||||
"Ориентир бюджета — «в пределах 1000» на эксперимент (токены — точно, труд — обсудить)",
|
||||
"Вход от студии: рассказ + свобода интерпретации, без пожеланий-ограничений",
|
||||
"Партнёр уже строит harness генерации мультиков — матч идеальный; видео-часть ведёт Влад",
|
||||
("Первый шаг — аудиоспектакль (дешевле и предсказуемее), затем видео", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Творческий метод Леонида", [
|
||||
"Хакатон ~2018: нейросеть на сольных песнях Леннона/Маккартни классифицировала битловские",
|
||||
"Пишет с ИИ (~80% своё / ~20% ИИ), честно маркирует «made with AI» на after.today",
|
||||
("«Литературные негры» и мастерская Микеланджело — ИИ-ассистированное творчество не ново, нова честность", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Экономика локального инференса", [
|
||||
"Леонид: DGX Spark — нормальная цена за «домашний интеллект»; коробки станут достоянием домов",
|
||||
"Алекс: облачный always-on — ок для большинства; топ-модели для разработки всё равно выигрывают",
|
||||
("Сошлись на гибриде: локально — быстрая генерация, облако — тяжёлые продуктовые задачи", 1),
|
||||
("У студии в планах свой inference-сервер (в ~6 раз быстрее)", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Как строить AI-компанию", [
|
||||
"Леонид: «Vision — говно, execution — всё». Из короткого промпта не вырастет сложная система",
|
||||
"Алекс: ядро студии — AI-ядро, перестраивающее паттерны (64) при смене контекста — главный долгосрочный актив",
|
||||
"Стратегия: бизнес на нейро-инфраструктуре с начала → перевод на автопилот; незаменим только «CEO-решатель»",
|
||||
("«Изюминка студии — в AI-ядре» — Леонид", 1),
|
||||
("Методология deep-tech: продавать ценность сразу, не влюбляясь в R&D", 1),
|
||||
], kicker=K)
|
||||
add_slide(prs, "Договорённости", [
|
||||
"1. Пилот анимации по рассказу Леонида: токены покрывает Леонид (до ~1000), ведёт Влад; Alex вернётся с конкретным предложением",
|
||||
"2. Формат разговора продолжается — Леонид открыт к следующим сессиям",
|
||||
"3. Голоса/доли за прошлые инвестиции — Леонид отказался: «инвестирую из философских соображений, не создавайте ожиданий»",
|
||||
"4. Леонид посмотрит 35-минутную презентацию студии и вернётся с фидбэком",
|
||||
], kicker=K)
|
||||
add_slide(prs, "Что делать студии дальше", [
|
||||
"Подготовить оффер по мульт-пилоту: объём, этап аудиоспектакля, смета токенов",
|
||||
"Подключить партнёрский harness генерации мультиков — Леонид как первый клиентский кейс",
|
||||
"Зафиксировать кейс «агент-визуализатор на внешнем звонке» — первое живое демо, положительный фидбэк",
|
||||
], kicker=K)
|
||||
prs.save("out/leonid_meeting143.pptx")
|
||||
|
||||
# --- Pitch deck: Simbiot / Seedance ---
|
||||
prs2 = Presentation(); prs2.slide_width = Inches(13.333); prs2.slide_height = Inches(7.5)
|
||||
K2 = "Simbiot · питч"
|
||||
add_slide(prs2, "Simbiot — общий харнес организации", [
|
||||
"Стартап за неделю получает не одного ассистента, а команду ИИ-агентов:",
|
||||
"один отвечает клиентам, второй пишет код, третий следит за деньгами и метриками",
|
||||
"Слой поверх Hermes, Claude Code и других — вместо зоопарка агентов и харнесов",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Проблема", [
|
||||
"Фаундер собирает зоопарк из агентов и харнесов вручную",
|
||||
"Ушёл разработчик — ушла вся магия: знания и автоматизации не переживают людей",
|
||||
"Каждый новый инструмент — ещё один остров, не связанный с остальными",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Решение", [
|
||||
"Simbiot координирует флот агентов и фиксирует решения компании",
|
||||
"Удачные приёмы копятся как «гены» — остаются только те, что приносят прибыль",
|
||||
"Компания превращается в самообучающуюся нейро-инфраструктуру",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Тракшен", [
|
||||
"Уже гоняется на семи собственных продуктах студии",
|
||||
"AI-ядро: 64 паттерна, перестраивающихся при смене контекста",
|
||||
"Агент-визуализатор, RAG по знаниям, собственный LM-роутер — в продакшене",
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Видео (Seedance), 3 шота", [
|
||||
"Shot 1 «зоопарк в хаосе»: тёмная серверная, хаотичные голограммы и глитчи (0–10 сек)",
|
||||
"Shot 2 «orchestration»: хаос сходится в фиолетовую сферу, панели выстраиваются в сетку (10–20 сек)",
|
||||
"Shot 3 «финал»: фаундер у стены дашбордов с растущими revenue-графиками, отъезд в город (20–30 сек)",
|
||||
("Стиль: dark cinematic, #0a0c12 + violet #7c6cf5, film grain, 4k, 16:9", 1),
|
||||
], kicker=K2)
|
||||
add_slide(prs2, "Строй компанию — а не зоопарк ботов", [
|
||||
"Simbiot · Кооператив 42 · simbiot",
|
||||
"Хронометраж питча: ~35 сек (15 сек — первый и последний абзац)",
|
||||
], kicker=K2)
|
||||
prs2.save("out/simbiot_pitch.pptx")
|
||||
print("OK")
|
||||
168
scripts/spa_fill.py
Normal file
168
scripts/spa_fill.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env python3
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# SPA filler — Studio_Participation_Agreement_2026-09 → per-founder
|
||||
# Задачи t-261008143050 (Siarhei Zhukovich, когорта с 16-го) и
|
||||
# t-261008144131 (7 участников 42 Labs за 2026-09).
|
||||
# Паттерн подстановки — как _contract_docx_bytes в /root/coop/app.py.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
import os, sys, copy
|
||||
import docx
|
||||
|
||||
TEMPLATE = "/root/coop/legal/Studio_Participation_Agreement_2026-09.docx"
|
||||
OUT = "/root/coop/legal"
|
||||
|
||||
DRAFT_LINE = "DRAFT TEMPLATE — for review by U.S. counsel before first use. Not legal advice."
|
||||
|
||||
# Перепривязка платежного графика на 16-е число (когорты с 16-го, t-261008143050)
|
||||
SIXTEENTH = [
|
||||
("no later than the 1st day of the first month of participation",
|
||||
"no later than the 16th day of the first month of participation"),
|
||||
("each subsequent Monthly Fee no later than the 1st day of the corresponding month of participation",
|
||||
"each subsequent Monthly Fee no later than the 16th day of the corresponding month of participation"),
|
||||
("If the Agreement is signed after the 1st, the Program Start is the 1st day of the next calendar month",
|
||||
"If the Agreement is signed after the 16th, the Program Start is the 16th day of the next calendar month"),
|
||||
("for Monthly Fee tranches, the 1st day of the participation month",
|
||||
"for Monthly Fee tranches, the 16th day of the participation month"),
|
||||
("takes effect on the 1st day of the next participation month",
|
||||
"takes effect on the 16th day of the next participation month"),
|
||||
("payable in advance by the 1st of each participation month",
|
||||
"payable in advance by the 16th of each participation month"),
|
||||
("the 1st day of the month of signature if signed on the 1st, otherwise the 1st day of the next calendar month",
|
||||
"the 16th day of the month of signature if signed on the 16th, otherwise the 16th day of the next calendar month"),
|
||||
]
|
||||
|
||||
|
||||
def founder_replacements(f):
|
||||
"""f: dict(name, passport, pid, address, email, project, one_line, rev_share,
|
||||
effective, cohort, program_start, sixteenth: bool)"""
|
||||
r = {
|
||||
"[DATE]": f["effective"],
|
||||
"[FULL LEGAL NAME]": f["name"],
|
||||
"[NAME]": f["name"],
|
||||
"[EMAIL]": f.get("email") or "[EMAIL]",
|
||||
"cohort [MONTH YEAR]": f"cohort {f['cohort']}",
|
||||
"1 [MONTH] [YEAR]": f["program_start"],
|
||||
"[MONTH YEAR]": f["cohort"],
|
||||
}
|
||||
if f.get("rev_share"):
|
||||
r["[X]"] = str(f["rev_share"])
|
||||
if f.get("project"):
|
||||
r["[PROJECT NAME]"] = f["project"]
|
||||
if f.get("one_line"):
|
||||
r["[ONE LINE]"] = f["one_line"]
|
||||
# Строка Founder: имя + статус + паспорт + адрес одной подстановкой
|
||||
line_old = "[FULL LEGAL NAME], an individual / [ENTITY TYPE], residing at [ADDRESS], [COUNTRY]"
|
||||
line_new = (f"{f['name']}, an individual, passport of the Republic of Belarus "
|
||||
f"{f.get('passport') or '[PASSPORT NO.]'} (personal identification No. "
|
||||
f"{f.get('pid') or '[PERSONAL ID NO.]'}), residing at "
|
||||
f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus")
|
||||
r[line_old] = line_new
|
||||
# фолбэк, если строка разбита рант differently
|
||||
r["an individual / [ENTITY TYPE]"] = "an individual"
|
||||
# паспорт/ид.номер вставляются после схлопывания individual-строки
|
||||
# (порядок ключей dict важен: этот ключ применяется после предыдущего)
|
||||
pass_line = (f"passport of the Republic of Belarus {f.get('passport') or '[PASSPORT NO.]'}"
|
||||
f" (personal identification No. {f.get('pid') or '[PERSONAL ID NO.]'}), residing at")
|
||||
r["an individual, residing at"] = f"an individual, {pass_line}"
|
||||
r["[ADDRESS], [COUNTRY]"] = f"{f.get('address') or '[ADDRESS]'}, Republic of Belarus"
|
||||
r["[ADDRESS]"] = f.get("address") or "[ADDRESS]"
|
||||
r["[COUNTRY]"] = "Republic of Belarus"
|
||||
return r
|
||||
|
||||
|
||||
def fill(f, out_docx):
|
||||
doc = docx.Document(TEMPLATE)
|
||||
reps = founder_replacements(f)
|
||||
if f.get("sixteenth"):
|
||||
reps = dict(SIXTEENTH) | reps
|
||||
reps[DRAFT_LINE] = ""
|
||||
for p in doc.paragraphs:
|
||||
if not p.runs:
|
||||
continue
|
||||
full = "".join(r.text for r in p.runs)
|
||||
new = full
|
||||
for k, v in reps.items():
|
||||
if k in new:
|
||||
new = new.replace(k, v)
|
||||
if new != full:
|
||||
p.runs[0].text = new
|
||||
for r in p.runs[1:]:
|
||||
r.text = ""
|
||||
doc.save(out_docx)
|
||||
return out_docx
|
||||
|
||||
|
||||
def to_pdf(docx_path):
|
||||
import subprocess, tempfile
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
r = subprocess.run(["soffice", "--headless", "--convert-to", "pdf", "--outdir", td, docx_path],
|
||||
capture_output=True, timeout=90)
|
||||
pdf = os.path.join(td, os.path.basename(docx_path).replace(".docx", ".pdf"))
|
||||
if os.path.exists(pdf):
|
||||
dst = docx_path.replace(".docx", ".pdf")
|
||||
open(dst, "wb").write(open(pdf, "rb").read())
|
||||
return dst
|
||||
return None
|
||||
|
||||
|
||||
def leftover_placeholders(docx_path):
|
||||
import re
|
||||
d = docx.Document(docx_path)
|
||||
found = set()
|
||||
for p in d.paragraphs:
|
||||
found.update(re.findall(r"\[[A-Z][A-Z0-9 /.,%()\[\]-]*\]", p.text))
|
||||
return sorted(found)
|
||||
|
||||
|
||||
FOUNDERS = [
|
||||
# ── t-261008143050: Siarhei Zhukovich, когорта с 16-го ──
|
||||
dict(file="SPA_Siarhei_Zhukovich", name="Siarhei Zhukovich",
|
||||
passport="AB3393306", pid=None, # ид. номер — нет в базе, плейсхолдер
|
||||
address="28 July str., 33B-42, Brest", email=None,
|
||||
project="podrabotki.by (rebranding to StaffGo)",
|
||||
one_line="a platform for hiring line staff: employment/contract placement and turnkey contractor paperwork (documents and payments)",
|
||||
rev_share=50, effective="October 16, 2026", cohort="October 2026",
|
||||
program_start="16 October 2026", sixteenth=True),
|
||||
# ── t-261008144131: участники 42 Labs за 2026-09 (lab_debts) ──
|
||||
dict(file="SPA_Alex_Shkor", name="Alex Shkor", passport=None, pid=None,
|
||||
address=None, email="alex@shkor.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Vlad", name="Vlad", passport=None, pid=None,
|
||||
address=None, email="vla2music.com@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Yura", name="Yura", passport=None, pid=None,
|
||||
address=None, email="yura28440@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Stas_Dashchinskiy", name="Stas Dashchinskiy", passport=None, pid=None,
|
||||
address=None, email="dashchinskiystas@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Alexander", name="Александр", passport=None, pid=None,
|
||||
address=None, email="Justgbox@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Melnikau", name="Andrei Melnikau", passport="MP2848482",
|
||||
pid="3120386A064PB1", address=None, email="ex3.mac@gmail.com",
|
||||
project="tYESter",
|
||||
one_line="a family of software tools for testing computer hardware components without the need to purchase additional equipment",
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
dict(file="SPA_Dmitry_Siasitski", name="Dmitry Siasitski", passport=None, pid=None,
|
||||
address=None, email="sesitski.dmitry@gmail.com", project=None, one_line=None,
|
||||
rev_share=None, effective="September 1, 2026", cohort="September 2026",
|
||||
program_start="1 September 2026", sixteenth=False),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
only = sys.argv[1:] or None
|
||||
for f in FOUNDERS:
|
||||
if only and f["file"] not in only:
|
||||
continue
|
||||
docx_path = os.path.join(OUT, f["file"] + ".docx")
|
||||
fill(f, docx_path)
|
||||
pdf = to_pdf(docx_path)
|
||||
print(f"{f['file']}: docx={'OK' if os.path.exists(docx_path) else 'FAIL'} "
|
||||
f"pdf={'OK' if pdf else 'FAIL'} leftover={leftover_placeholders(docx_path)}")
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
|---|---|---|
|
||||
| `meta-skill-factory` | Метаскилл: превращение повторяющейся работы в скилл репо (шаги, чеклисты, критерии качества) | Заметил, что делаю одну и ту же работу 2+ раз — прежде чем делать руками в третий |
|
||||
| `presale-landing` | Производство пресейл/продуктовых лендингов: структура, честные мокапы, Stripe CTA, quality gates | Задача на лендинг/пресейл-страницу или её v2 |
|
||||
| `gcal` | Google Calendar: команда из TG «сделай звонок на X» → событие с приглашениями в рабочий день (цикл 3 через 3); инструменты tools/gcal.py | Просьба спланировать звонок/встречу/событие, вопрос по графику |
|
||||
|
||||
## Сторонние наборы
|
||||
|
||||
|
|
|
|||
73
skills/compact-landing/SKILL.md
Normal file
73
skills/compact-landing/SKILL.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
description: Build compact premium landing pages with clear CTA hierarchy, quiet typography, and minimal visual noise. Install from Danilaa1 in one command.
|
||||
---
|
||||
# Compact Landing
|
||||
Build a small, finished product world—not a generic marketing template. Preserve compactness and clarity; reinvent the visible design language for each product.
|
||||
## Workflow
|
||||
1. Inspect the product, supplied content, current stack, and existing design language. Reuse installed tools and components.
|
||||
2. Ask for button size first—compact, medium, or large—unless the user already answered or asked you to choose.
|
||||
3. Ask at most two adaptive questions only when their answers would materially change the result. Good subjects: brand mood, content depth, corner character, or motion level.
|
||||
4. Create a design fingerprint before coding. If this skill includes scripts and Node is available, run `node scripts/pick-direction.mjs`. Otherwise choose from `references/style-dna.md` without taking the first option in every list.
|
||||
5. Adapt the fingerprint to product meaning. Treat picker output as a creative constraint, not a command to make incoherent choices.
|
||||
6. Identify one product-native signature artifact: a real demo, visualized output, interactive sample, comparison, miniature workflow, data specimen, or useful control. This should create identity more than decoration does.
|
||||
7. Implement the smallest complete page. Prefer HTML/CSS and existing dependencies. Add JavaScript only for behavior the page actually needs.
|
||||
8. Verify mobile layout, keyboard focus, reduced motion, stable dynamic states, and the real primary action.
|
||||
If the user wants immediate execution, choose the button size and fingerprint yourself. Do not stop for a questionnaire.
|
||||
## Design Fingerprint
|
||||
Define these axes before implementation:
|
||||
- **Composition:** field note, offset rail, split pamphlet, framed console, compact catalogue, stacked specimen, or another product-specific silhouette.
|
||||
- **Measure:** choose a fitting compact width, usually `420-720px`; do not default to `440px` every time.
|
||||
- **Palette:** warm paper, cool fog, monochrome ink, dark charcoal, product-tinted, or playful light.
|
||||
- **Typography:** grotesk + mono, humanist sans + mono, editorial serif + sans, rounded sans + mono, condensed + sans, or purposeful all-mono.
|
||||
- **Geometry:** sharp, machined, balanced, friendly, soft, or mixed/concentric radii.
|
||||
- **Surface grammar:** whitespace, flat dividers, inset hairlines, layered rings, tinted panels, paper rules, or technical frames. Pick one dominant grammar.
|
||||
- **Motif:** emoji punctuation, ASCII marks, indices, stamps, doodles, colored dots, tiny object icons, or none.
|
||||
- **Motion:** choose one entrance behavior and one or two interaction behaviors.
|
||||
Variation must change the first-screen silhouette, not merely colors or labels. Ensure at least four axes differ from the old canonical recipe: centered `440px` column, neutral white/zinc, Geist + Geist Mono, `6-10px` radii, inset hairline cards, Instrument order, staggered rise.
|
||||
Do not randomize components independently. Choose one coherent art direction, then derive tokens from it. Use only one or two surprising details; repetition inside the page creates coherence.
|
||||
Read `references/style-dna.md` for compatible ranges, direction families, motion patterns, and review checks.
|
||||
## Invariants
|
||||
- Keep the page compact in scope and information density, even when its canvas is wider or split.
|
||||
- Put the primary action in the first viewport. Repeat it only when doing so removes friction.
|
||||
- Use clear type hierarchy, concrete copy, and enough contrast.
|
||||
- Use real product content. Avoid fake metrics, vague claims, filler testimonials, and decorative dashboards.
|
||||
- Extend visually tiny controls to at least a `40px` hit area without making them look oversized.
|
||||
- Show a visible `:focus-visible` state and preserve semantic HTML.
|
||||
- Use tabular numerals for changing numbers.
|
||||
- Respect `prefers-reduced-motion` and keep the reduced experience fully usable.
|
||||
- Never add a dependency for a small effect CSS or the current stack can handle.
|
||||
## Zero-Shift Motion
|
||||
- Animate only `transform`, `opacity`, and `filter`. Never use `transition: all`.
|
||||
- Reserve dimensions before animation: fixed/min heights, `aspect-ratio` for media, fixed icon slots, width-locked changing labels, and tabular numerals.
|
||||
- Overlay alternate labels, icons, tabs, and panels in the same grid area or fixed box. Do not insert delayed content into document flow.
|
||||
- Keep padding, borders, font size, font weight, line height, grid tracks, and flex sizing stable across hover and state changes.
|
||||
- Prefer transforms for toggles, drawers, traces, and decorative motion. Absolute-position decorative particles so they never affect flow.
|
||||
- Self-host fonts when practical. Otherwise use metric-compatible fallbacks and avoid hiding the page while fonts load.
|
||||
- Gate hover-only effects behind `(hover: hover) and (pointer: fine)`.
|
||||
## Buttons
|
||||
Apply one scale consistently:
|
||||
- **Compact:** `24-28px` visual height, `11-12px` label.
|
||||
- **Medium:** `32-36px` visual height, `12.5-13px` label.
|
||||
- **Large:** `40-44px` visual height, `13.5-14px` label.
|
||||
Use `scale(0.96)` for press feedback. For wide command controls, use `0.98`. Keep label and icon slots stable during copied/loading/success states.
|
||||
For `llms.txt` or assistant-copy actions, use a small mono button only when the product needs it. Reuse the project's icon system or a CSS glyph; do not embed SVG examples in skill output.
|
||||
## Purposeful Personality
|
||||
Emoji are allowed when they fit the product. Use `1-3` as punctuation, labels, tiny stickers, or state cues—not as a substitute for structure or iconography. Reserve their boxes so platform glyph differences do not move layout.
|
||||
Color may be playful, dark, editorial, or product-tinted. Give every accent a role and repeat it. Avoid default purple-blue AI gradients, uncontrolled rainbows, and random decorative color.
|
||||
Motion should explain state, focus attention, or express the product metaphor. A fixed-size trace, ambient floating mark, slot swap, clipped reveal, or state color wash can feel alive without causing reflow. Avoid perpetual motion everywhere.
|
||||
## Avoid
|
||||
- Reusing one section order, narrow width, palette, radius family, or component set across unrelated products.
|
||||
- Generic oversized heroes, three equal feature cards, badge clouds, and feature grids by reflex.
|
||||
- Copying the Cuelume waveform, cream palette, confetti trio, or exact layout unless the product genuinely calls for them.
|
||||
- Making every surface a card or every control a pill.
|
||||
- Decorative animation that competes with the primary action.
|
||||
- Heavy animation libraries for opacity, transforms, tabs, counters, or simple reveals.
|
||||
## Final Check
|
||||
- Fingerprint chosen before code; at least four high-salience axes differ from the canonical recipe.
|
||||
- Product-native signature artifact is visible and useful.
|
||||
- First viewport has a distinct silhouette and clear primary action.
|
||||
- Radius, palette, type, surface, motif, and motion form one coherent direction.
|
||||
- Mobile becomes a clean single column without horizontal overflow.
|
||||
- Dynamic content, media, icons, labels, and panels have reserved geometry.
|
||||
- Keyboard, focus, contrast, touch targets, and reduced motion work.
|
||||
- Build and the primary interaction pass using the project's existing verification path.
|
||||
66
skills/gcal/SKILL.md
Normal file
66
skills/gcal/SKILL.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
name: gcal
|
||||
description: Use when Alex asks in TG to schedule a call/meeting ("сделай звонок на X с Владой", "запланируй созвон", "поставь встречу") — creates a Google Calendar event with guest invites in a workday slot (Alex's «3 через 3» cycle), or checks the schedule. Powered by tools/gcal.py.
|
||||
metadata:
|
||||
origin: task t-261008155436-a658e9, messages m-516173943/m-516173944
|
||||
requires: secrets/gcal-service-account.json (service account key; until provided — dry-run/planning only)
|
||||
---
|
||||
|
||||
# gcal: команда из TG → событие в календаре Alex
|
||||
|
||||
Одна команда из TG должна решаться одним прогоном этого скилла:
|
||||
интеншн → валидный слот (рабочий день!) → событие с приглашением → ответ в чат.
|
||||
|
||||
## Шаг 1 — разобрать команду
|
||||
|
||||
Из сообщения извлечь:
|
||||
- **что**: тема (по умолчанию «Звонок: <тема/имя гостя>»);
|
||||
- **когда**: день (сегодня/завтра/дата) и время; длительность (по умолчанию 30 мин);
|
||||
- **кого**: email гостей. Известные контакты: Влада — videoprodshema@gmail.com.
|
||||
Email НЕ известен → спросить у Alex ДО создания (угадывать адрес нельзя);
|
||||
- **время**: Alex в Минске (UTC+3). Время из TG трактовать как минское,
|
||||
в инструмент передавать UTC.
|
||||
|
||||
## Шаг 2 — валидация слота (строго до создания)
|
||||
|
||||
```
|
||||
./venv/bin/python tools/gcal.py next-workdays # ближайшие рабочие
|
||||
./venv/bin/python tools/gcal.py create ... --dry-run # что попадёт в Google
|
||||
```
|
||||
|
||||
Guard'ы (встроены в create, не обходить):
|
||||
1. **Только рабочий день** — цикл «3 через 3» (docs/alex-schedule.md).
|
||||
Якорь цикла: env GCAL_WORK_ANCHOR (по умолчанию 2026-10-02). Сдвиг цикла
|
||||
Alex сообщил в TG → обновить docs/alex-schedule.md и якорь.
|
||||
2. **Не позже 2ч до начала** (GCAL_MIN_LEAD_MIN=120) — анти-паттерн рекордера
|
||||
(t-261008155708: встречи, созданные <2 мин до старта, не заходятся).
|
||||
|
||||
Выходной/поздний слот → НЕ создавать, ответить Alex с ближайшими рабочими
|
||||
днями и попросить подтвердить слот.
|
||||
|
||||
## Шаг 3 — создать событие
|
||||
|
||||
```
|
||||
./venv/bin/python tools/gcal.py create \
|
||||
--title "Звонок: Влада" --day YYYY-MM-DD --time HH:MM \
|
||||
--duration 30 --guests "videoprodshema@gmail.com" --timezone Europe/Minsk
|
||||
```
|
||||
|
||||
`sendUpdates=all` — приглашения уходят гостям автоматически.
|
||||
Ответ в чат: ссылка на событие (htmlLink), слот, гости.
|
||||
|
||||
## Ключ (один раз, если не настроен)
|
||||
|
||||
`tools/gcal.py check` печатает готовность и шаги:
|
||||
Google Cloud → Calendar API → service account → JSON-ключ → дать
|
||||
client_email право «Вносить изменения» на календаре Alex → положить в
|
||||
`secrets/gcal-service-account.json` (0600). Ключа нет → работать dry-run'ом
|
||||
и попросить Alex прислать ключ (это может сделать только владелец аккаунта).
|
||||
|
||||
## Критерии качества
|
||||
|
||||
- [ ] Слот — рабочий день цикла; иначе отказ с предложением ближайших рабочих
|
||||
- [ ] До начала ≥2ч (или осознанный --force по явной просьбе Alex)
|
||||
- [ ] Гости получили приглашения (sendUpdates=all), email не угадан
|
||||
- [ ] В ответе Alex — время в его зоне (Минск) и ссылка на событие
|
||||
- [ ] `check` зелёный; ключ лежит в secrets/ с правами 0600
|
||||
111
tests/test_msg_artifact_race.py
Normal file
111
tests/test_msg_artifact_race.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""t-261009160906: анти-гонка уровней min/hour на входящих TG (hermetic).
|
||||
|
||||
Кейс 09.10: Alex попросил концепт (m-516174013). Мин-тик ответил и доставил
|
||||
свой файл (16:01). Часовой тик, не видя ответа мин-уровня, сгенерировал СВОЙ
|
||||
файл и доставил вторым (16:06) — Alex получил два разных файла на один запрос.
|
||||
Dedup по artifact_id=file:<путь> не ловит: два разных файла = два разных ключа.
|
||||
|
||||
Фикс: связка «артефакт → входящее» (-m m-NNN у tg-deliver-file, msg= у
|
||||
deliver_artifact, reply_artifact в read model). Если на входящее уже отвечено
|
||||
иным артефактом — SKIP с событием. force обходит (ручная повторная доставка).
|
||||
|
||||
Запуск: ./venv/bin/python tests/test_msg_artifact_race.py
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# ── изоляция: своя БД в /tmp (prod не трогаем) ──
|
||||
_DST = "/tmp/simbiot-test-msg-artifact-race.db"
|
||||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||||
if os.path.exists(p):
|
||||
os.unlink(p)
|
||||
os.environ["SIMBIOT_DB"] = _DST
|
||||
sys.path.insert(0, "/root/simbiot-harness")
|
||||
|
||||
from harness import es, commands
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(name, cond, extra=""):
|
||||
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
|
||||
if not cond:
|
||||
fails.append(name)
|
||||
|
||||
|
||||
es.init_db()
|
||||
|
||||
# ── подготовка: входящее m-990001, на которое мин-уровень уже ответил ──
|
||||
es.append_event("inbox", "m-990001", "MessageReceived", {
|
||||
"update_id": 990001, "chat_id": 42, "message_id": 1, "user_id": 1,
|
||||
"text": "сделай концепт в html", "kind": "text", "date": 1791560000,
|
||||
})
|
||||
es.append_event("inbox", "m-990001", "ReplySent", {
|
||||
"to_chat": 42, "text": "готово, отправил файлом", "tg_ok": True, "channel": "tg",
|
||||
})
|
||||
|
||||
# файлы-двойники гонки: мин-уровень и часовой
|
||||
_F1 = "/tmp/simbiot-test-race-min.html"
|
||||
_F2 = "/tmp/simbiot-test-race-hour.html"
|
||||
for f in (_F1, _F2):
|
||||
with open(f, "w") as fh:
|
||||
fh.write("<html><body>r</body></html>")
|
||||
|
||||
sends = []
|
||||
|
||||
# 1) мин-уровень доставил свой файл с привязкой к сообщению
|
||||
ok1 = commands.deliver_artifact(f"file:{_F1}", kind="file", send=lambda: (sends.append(_F1) or True),
|
||||
owner="test:min", caption="мин", msg="m-990001")
|
||||
check("min-level file delivered", ok1 and sends == [_F1], sends)
|
||||
|
||||
model = es.run_projections()
|
||||
m = model["inbox"]["m-990001"]
|
||||
check("read model binds reply_artifact", m.get("reply_artifact") == f"file:{_F1}", m.get("reply_artifact"))
|
||||
|
||||
# 2) часовой уровень пытается доставить СВОЙ файл на то же входящее → SKIP
|
||||
ok2 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
|
||||
owner="test:hour", caption="час", msg="m-990001")
|
||||
check("hour-level duplicate SKIPped", ok2 is False and sends == [_F1], sends)
|
||||
|
||||
db = es._conn()
|
||||
skip = list(db.execute(
|
||||
"SELECT data FROM events WHERE event_type='DeliverySkipped' AND data LIKE '%msg-already%'"))
|
||||
check("DeliverySkipped event with reason", len(skip) == 1, len(skip))
|
||||
|
||||
# 3) force обходит защиту (ручная повторная доставка — явное решение человека)
|
||||
ok3 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
|
||||
owner="test:hour", caption="час", force=True, msg="m-990001")
|
||||
check("force bypasses guard", ok3 and sends == [_F1, _F2], sends)
|
||||
|
||||
# 4) доставки БЕЗ привязки к сообщению не задеваются (обратная совместимость)
|
||||
ok4 = commands.deliver_artifact("file:/tmp/simbiot-test-race-unbound.html", kind="file",
|
||||
send=lambda: (sends.append("unbound") or True),
|
||||
owner="test", caption="x")
|
||||
check("unrelated delivery unaffected", ok4 and sends[-1] == "unbound", sends)
|
||||
|
||||
# 5) второе входящее без артефакта: доставка с привязкой проходит свободно
|
||||
es.append_event("inbox", "m-990002", "MessageReceived", {
|
||||
"update_id": 990002, "chat_id": 42, "message_id": 2, "user_id": 1,
|
||||
"text": "ещё один запрос", "kind": "text", "date": 1791560100,
|
||||
})
|
||||
es.append_event("inbox", "m-990002", "ReplySent", {
|
||||
"to_chat": 42, "text": "ок", "tg_ok": True, "channel": "tg",
|
||||
})
|
||||
ok5 = commands.deliver_artifact("file:/tmp/simbiot-test-race-second.html", kind="file",
|
||||
send=lambda: (sends.append("second") or True),
|
||||
owner="test:hour", caption="y", msg="m-990002")
|
||||
check("fresh msg accepts its artifact", ok5 and sends[-1] == "second", sends)
|
||||
|
||||
# 6) CLI: tg-deliver-file -m m-990001 с чужим артефактом → SKIP, выход 0
|
||||
r = subprocess.run(
|
||||
["./venv/bin/python", "cli.py", "tg-deliver-file", _F2, "-m", "m-990001",
|
||||
"-c", "dup", "-k", "file:" + _F2],
|
||||
capture_output=True, text=True, cwd="/root/simbiot-harness")
|
||||
check("CLI SKIP on raced msg", r.returncode == 0 and "SKIP" in r.stdout,
|
||||
r.stdout.strip()[:120] + r.stderr.strip()[-200:])
|
||||
|
||||
print("=" * 40)
|
||||
print("FAILS:", len(fails), fails if fails else "— все проверки прошли")
|
||||
sys.exit(1 if fails else 0)
|
||||
290
tools/gcal.py
Executable file
290
tools/gcal.py
Executable file
|
|
@ -0,0 +1,290 @@
|
|||
#!/usr/bin/env python3
|
||||
"""gcal.py — Google Calendar для харнеса: create-event / list / check (t-261008155436).
|
||||
|
||||
Команда из TG «сделай звонок на X с Владой» → минутный тик грузит скилл
|
||||
skills/gcal/SKILL.md → зовёт этот инструмент → событие в календаре Alex
|
||||
с приглашением гостям, слот только на рабочий день (цикл «3 через 3»).
|
||||
|
||||
Автоматизация (service account, JWT → access token, REST v3 — без google-клиентов):
|
||||
1) Alex: console.cloud.google.com → новый проект → включить Calendar API →
|
||||
«Service accounts» → создать → ключ JSON → прислать файл.
|
||||
2) В календаре Google: Настройки → «Настройки для моего календаря» →
|
||||
«Предоставить доступ» → добавить client_email ключа с ролью
|
||||
«Вносить изменения» (make changes to events).
|
||||
3) Положить ключ: /root/simbiot-harness/secrets/gcal-service-account.json
|
||||
(или env GCAL_SERVICE_ACCOUNT_FILE). Файл 0600.
|
||||
4) ./gcal.py check — готовность. Готово: скилл создаёт события без человека.
|
||||
|
||||
Если ключа нет — работают --dry-run, check, next-workdays (планирование слотов).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
REPO = HERE.parent
|
||||
DEFAULT_KEY = REPO / "secrets" / "gcal-service-account.json"
|
||||
TOKEN_CACHE = Path("/tmp/.gcal_access_token.json")
|
||||
|
||||
# ── График Alex: цикл «3 через 3» ─────────────────────────────────────────────
|
||||
# docs/alex-schedule.md: 3 рабочих, 3 выходных. Якорь выведен из m-516173591
|
||||
# (2026-10-03: «сегодня и завтра рабочие, далее 3 выходных» → 10-02 первый
|
||||
# рабочий цикла). Проверка: 10-08 = (10-08−10-02)=6 → 6%6=0 → рабочий (совпало
|
||||
# с «сегодня/завтра рабочие» от 10-08). Сдвиг цикла: env GCAL_WORK_ANCHOR=YYYY-MM-DD
|
||||
ANCHOR = os.environ.get("GCAL_WORK_ANCHOR", "2026-10-02")
|
||||
PERIOD = 6
|
||||
WORK_DAYS = {0, 1, 2} # смещения внутри цикла: 0,1,2 — рабочие; 3,4,5 — выходные
|
||||
|
||||
MIN_LEAD_MIN = int(os.environ.get("GCAL_MIN_LEAD_MIN", "120")) # анти-паттерн рекордера: не позже 2ч до начала
|
||||
|
||||
|
||||
def _anchor() -> date:
|
||||
return datetime.strptime(ANCHOR, "%Y-%m-%d").date()
|
||||
|
||||
|
||||
def is_workday(d: date) -> bool:
|
||||
return ((d - _anchor()).days % PERIOD) in WORK_DAYS
|
||||
|
||||
|
||||
def next_workdays(n: int = 6, from_day: date | None = None) -> list[date]:
|
||||
d = from_day or date.today()
|
||||
out: list[date] = []
|
||||
while len(out) < n:
|
||||
if is_workday(d):
|
||||
out.append(d)
|
||||
d += timedelta(days=1)
|
||||
return out
|
||||
|
||||
|
||||
# ── Креды и токен (service account → JWT → access token) ─────────────────────
|
||||
def _key_path(explicit: str | None) -> Path:
|
||||
p = Path(explicit or os.environ.get("GCAL_SERVICE_ACCOUNT_FILE") or DEFAULT_KEY)
|
||||
return p
|
||||
|
||||
|
||||
def load_key(explicit: str | None = None) -> dict | None:
|
||||
p = _key_path(explicit)
|
||||
if not p.exists():
|
||||
return None
|
||||
return json.loads(p.read_text())
|
||||
|
||||
|
||||
def _b64(o: dict | bytes) -> str:
|
||||
raw = json.dumps(o).encode() if isinstance(o, dict) else o
|
||||
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
|
||||
|
||||
def get_access_token(key: dict, force: bool = False) -> str:
|
||||
"""JWT (RS256) → Google OAuth2 → access token (кэш ~50 мин)."""
|
||||
if not force and TOKEN_CACHE.exists():
|
||||
try:
|
||||
c = json.loads(TOKEN_CACHE.read_text())
|
||||
if c.get("exp", 0) > time.time() + 120 and c.get("iss") == key["client_email"]:
|
||||
return c["token"]
|
||||
except Exception:
|
||||
pass
|
||||
import httpx # уже в venv харнеса
|
||||
|
||||
now = int(time.time())
|
||||
claim = {
|
||||
"iss": key["client_email"],
|
||||
"scope": "https://www.googleapis.com/auth/calendar",
|
||||
"aud": "https://oauth2.googleapis.com/token",
|
||||
"iat": now,
|
||||
"exp": now + 3600,
|
||||
}
|
||||
head = {"alg": "RS256", "typ": "JWT"}
|
||||
signing_input = f"{_b64(head)}.{_b64(claim)}".encode()
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
|
||||
pk = serialization.load_pem_private_key(key["private_key"].encode(), password=None)
|
||||
sig = pk.sign(signing_input, hashes.SHA256())
|
||||
jwt = f"{signing_input.decode()}.{_b64(sig)}"
|
||||
|
||||
r = httpx.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt},
|
||||
timeout=20,
|
||||
)
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError(f"token exchange failed: {r.status_code} {r.text[:200]}")
|
||||
tok = r.json()
|
||||
TOKEN_CACHE.write_text(json.dumps({"token": tok["access_token"], "exp": now + tok.get("expires_in", 3600),
|
||||
"iss": key["client_email"]}))
|
||||
TOKEN_CACHE.chmod(0o600)
|
||||
return tok["access_token"]
|
||||
|
||||
|
||||
def _api(method: str, path: str, key: dict, body: dict | None = None, params: dict | None = None) -> dict:
|
||||
import httpx
|
||||
|
||||
tok = get_access_token(key)
|
||||
r = httpx.request(method, f"https://www.googleapis.com/calendar/v3{path}",
|
||||
headers={"Authorization": f"Bearer {tok}", "Content-Type": "application/json"},
|
||||
json=body, params=params or {}, timeout=25)
|
||||
if r.status_code >= 400:
|
||||
raise RuntimeError(f"calendar api {r.status_code}: {r.text[:300]}")
|
||||
return r.json()
|
||||
|
||||
|
||||
# ── Действия ──────────────────────────────────────────────────────────────────
|
||||
def cmd_check(args) -> int:
|
||||
print(f"anchor (цикл 3 через 3): {ANCHOR} (env GCAL_WORK_ANCHOR для сдвига)")
|
||||
today = date.today()
|
||||
print(f"сегодня {today}: {'РАБОЧИЙ' if is_workday(today) else 'выходной'}")
|
||||
print("ближайшие рабочие:", ", ".join(d.isoformat() for d in next_workdays(4)))
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
p = _key_path(args.key)
|
||||
print(f"\nключ: НЕТ ({p})")
|
||||
print("шаги: Google Cloud → Calendar API → service account → JSON-ключ →")
|
||||
print(" доступ к календарю Alex для client_email (make changes) →")
|
||||
print(f" положить в {p} (0600). Затем: gcal.py check && gcal.py list")
|
||||
return 1
|
||||
print(f"\nключ: OK ({key.get('client_email', '?')})")
|
||||
try:
|
||||
cal = _api("GET", f"/calendars/{args.calendar}", key)
|
||||
print(f"календарь: {cal.get('summary')} ({cal.get('id')}) — доступ есть")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"доступ к календарю: НЕ УДАЛСЯ — {e}")
|
||||
print("→ дайте client_email право «Вносить изменения» в настройках календаря")
|
||||
return 2
|
||||
|
||||
|
||||
def _guard(slot_start: datetime) -> tuple[bool, str]:
|
||||
d = slot_start.date()
|
||||
if not is_workday(d):
|
||||
nxt = next_workdays(1, from_day=d + timedelta(days=1))
|
||||
sug = ", ".join(x.isoformat() for x in nxt)
|
||||
return False, f"{d.isoformat()} — выходной по циклу Alex «3 через 3». Ближайшие рабочие: {sug}"
|
||||
lead = (slot_start - datetime.now(timezone.utc)).total_seconds() / 60
|
||||
if lead < MIN_LEAD_MIN:
|
||||
return False, (f"до начала {int(lead)} мин < {MIN_LEAD_MIN} (анти-паттерн рекордера: "
|
||||
f"t-261008155708 — встречи <2 мин до старта не заходятся). Возьмите слот позже.")
|
||||
return True, "ok"
|
||||
|
||||
|
||||
def _parse_slot(day: str, hhmm: str, dur: int, tz_name: str) -> datetime:
|
||||
"""Локальное время дня в tz_name → момент UTC (API Google требует offset)."""
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
d = date.fromisoformat(day)
|
||||
hh, mm = map(int, hhmm.split(":"))
|
||||
return datetime(d.year, d.month, d.day, hh, mm, tzinfo=ZoneInfo(tz_name)).astimezone(timezone.utc)
|
||||
|
||||
|
||||
def cmd_create(args) -> int:
|
||||
start = _parse_slot(args.day, args.time, args.duration, args.timezone)
|
||||
ok, msg = _guard(start)
|
||||
if not ok and not args.force:
|
||||
print(f"ОТКАЗ: {msg}")
|
||||
return 3
|
||||
if not ok:
|
||||
print(f"ВНИМАНИЕ (force): {msg}")
|
||||
end = start + timedelta(minutes=args.duration)
|
||||
guests = [g.strip() for g in (args.guests or "").split(",") if g.strip()]
|
||||
event = {
|
||||
"summary": args.title,
|
||||
"description": args.description or "Создано харнесом Simbiot (skills/gcal, t-261008155436-a658e9)",
|
||||
"start": {"dateTime": start.isoformat(), "timeZone": args.timezone},
|
||||
"end": {"dateTime": end.isoformat(), "timeZone": args.timezone},
|
||||
"attendees": [{"email": g} for g in guests],
|
||||
"reminders": {"useDefault": False, "overrides": [
|
||||
{"method": "popup", "minutes": 30}, {"method": "popup", "minutes": 10},
|
||||
]},
|
||||
}
|
||||
if args.dry_run:
|
||||
print("DRY-RUN (в Google не писали). Событие:")
|
||||
print(json.dumps(event, ensure_ascii=False, indent=1))
|
||||
print(f"guard: {msg}; слот рабочий: {ok}")
|
||||
return 0
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
print("нет сервис-ключа Google — см. gcal.py check (шаги настройки)")
|
||||
return 1
|
||||
made = _api("POST", f"/calendars/{args.calendar}/events", key, body=event,
|
||||
params={"sendUpdates": "all"})
|
||||
print(f"СОЗДАНО: {made.get('htmlLink')}")
|
||||
print(f" {made.get('summary')} · {start:%Y-%m-%d %H:%M} UTC ({args.timezone}) · "
|
||||
f"гости: {', '.join(guests) or '—'}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_list(args) -> int:
|
||||
key = load_key(args.key)
|
||||
if not key:
|
||||
print("нет сервис-ключа Google — см. gcal.py check")
|
||||
return 1
|
||||
tmin = datetime.now(timezone.utc).isoformat()
|
||||
data = _api("GET", f"/calendars/{args.calendar}/events", key,
|
||||
params={"timeMin": tmin, "maxResults": args.limit, "orderBy": "startTime",
|
||||
"singleEvents": "true"})
|
||||
evs = data.get("items", [])
|
||||
if not evs:
|
||||
print("ближайших событий нет")
|
||||
return 0
|
||||
for e in evs:
|
||||
when = (e.get("start", {}).get("dateTime") or e.get("start", {}).get("date", ""))[:16]
|
||||
print(f" {when} {e.get('summary', '(без названия)')}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_next(args) -> int:
|
||||
for d in next_workdays(args.count):
|
||||
off = ((d - _anchor()).days % PERIOD)
|
||||
print(f" {d.isoformat()} рабочий #{off + 1}/3")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="Google Calendar для харнеса (create-event по команде из TG)")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
c = sub.add_parser("check", help="готовность: ключ, доступ, график")
|
||||
c.add_argument("--key", help="путь к JSON сервис-аккаунта")
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.set_defaults(fn=cmd_check)
|
||||
|
||||
c = sub.add_parser("create", help="создать событие (слот только рабочий день)")
|
||||
c.add_argument("--title", required=True)
|
||||
c.add_argument("--day", required=True, help="YYYY-MM-DD")
|
||||
c.add_argument("--time", required=True, help="HH:MM (UTC)")
|
||||
c.add_argument("--duration", type=int, default=30, help="минуты")
|
||||
c.add_argument("--guests", default="", help="email через запятую (приглашения)")
|
||||
c.add_argument("--description", default="")
|
||||
c.add_argument("--timezone", default=os.environ.get("GCAL_TZ", "UTC"))
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.add_argument("--key")
|
||||
c.add_argument("--dry-run", action="store_true", help="показать событие, в Google не писать")
|
||||
c.add_argument("--force", action="store_true", help="игнорировать guard (осознанно)")
|
||||
c.set_defaults(fn=cmd_create)
|
||||
|
||||
c = sub.add_parser("list", help="ближайшие события")
|
||||
c.add_argument("--limit", type=int, default=10)
|
||||
c.add_argument("--calendar", default=os.environ.get("GCAL_CALENDAR", "primary"))
|
||||
c.add_argument("--key")
|
||||
c.set_defaults(fn=cmd_list)
|
||||
|
||||
c = sub.add_parser("next-workdays", help="ближайшие рабочие дни (цикл 3 через 3)")
|
||||
c.add_argument("count", nargs="?", type=int, default=6)
|
||||
c.set_defaults(fn=cmd_next)
|
||||
|
||||
a = ap.parse_args()
|
||||
try:
|
||||
return a.fn(a)
|
||||
except Exception as e: # noqa: BLE001 — инструмент отвечает кодом, не трейсбеком
|
||||
print(f"ОШИБКА: {type(e).__name__}: {e}")
|
||||
return 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
{
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": "allow",
|
||||
"webfetch": "allow"
|
||||
},
|
||||
"share": "disabled"
|
||||
"permission": { "edit": "allow", "bash": "allow", "webfetch": "allow" },
|
||||
"share": "disabled",
|
||||
"provider": {
|
||||
"simbiot": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Simbiot Router (cil.by)",
|
||||
"options": { "baseURL": "https://router.cil.by/v1" },
|
||||
"models": {
|
||||
"gpt-5.2": {}, "claude-sonnet-4.6": {}, "gpt-4.1": {}, "o4-mini": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
~/.config/opencode/opencode.json.bak
Normal file
8
~/.config/opencode/opencode.json.bak
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"bash": "allow",
|
||||
"webfetch": "allow"
|
||||
},
|
||||
"share": "disabled"
|
||||
}
|
||||
2
~/.profile
Normal file
2
~/.profile
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export OPENAI_API_KEY=sk-9ffbac9384bc446eab3bbe58a4a98dc3f4b2347d341caf51a34ce93149fd8c57f1eff2065f89025e
|
||||
export OPENAI_BASE_URL=https://router.cil.by/v1
|
||||
Loading…
Reference in a new issue