mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 15:04:47 +00:00
feat(cabinet): «Спросить Елену» на этапах 3-5 + память + фиксация отклонений
- backend: канал messages (interview/qa), /api/ask с полным контекстом (интервью + документы + артефакт этапа + Q&A), tool record_deviation - хранилище отклонений (артефакт deviations: эталон Елены + выбор клиента + причина) - кабинет: свёрнутый док «Спросить Елену» на этапах 3-5, контекст этапа
This commit is contained in:
parent
30160a0999
commit
cf5e4f050a
@ -135,6 +135,7 @@ def init_db():
|
|||||||
# Миграции — добавляем колонки если нет (идемпотентно)
|
# Миграции — добавляем колонки если нет (идемпотентно)
|
||||||
for sql in [
|
for sql in [
|
||||||
"ALTER TABLE projects ADD COLUMN tg_chat_id TEXT",
|
"ALTER TABLE projects ADD COLUMN tg_chat_id TEXT",
|
||||||
|
"ALTER TABLE messages ADD COLUMN channel TEXT DEFAULT 'interview'",
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
con.execute(sql)
|
con.execute(sql)
|
||||||
@ -148,11 +149,59 @@ def get_project(token):
|
|||||||
return db().execute("SELECT * FROM projects WHERE token=?", (token,)).fetchone()
|
return db().execute("SELECT * FROM projects WHERE token=?", (token,)).fetchone()
|
||||||
|
|
||||||
def history(project_id):
|
def history(project_id):
|
||||||
|
# Только канал интервью — он питает билд-инструменты. Q&A по этапам сюда не попадает.
|
||||||
rows = db().execute(
|
rows = db().execute(
|
||||||
"SELECT role, content FROM messages WHERE project_id=? ORDER BY id", (project_id,)
|
"SELECT role, content FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id",
|
||||||
|
(project_id,)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [{"role": r["role"], "content": r["content"]} for r in rows]
|
return [{"role": r["role"], "content": r["content"]} for r in rows]
|
||||||
|
|
||||||
|
def interview_as_text(project_id):
|
||||||
|
"""Интервью в виде текста — для подмешивания в контекст Q&A без нарушения чередования ролей."""
|
||||||
|
rows = db().execute(
|
||||||
|
"SELECT role, content FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id",
|
||||||
|
(project_id,)
|
||||||
|
).fetchall()
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
out = ["ИНТЕРВЬЮ С КЛИЕНТОМ (этап 1, диагностика):"]
|
||||||
|
for r in rows:
|
||||||
|
who = "Клиент" if r["role"] == "user" else "Елена"
|
||||||
|
out.append(f"{who}: {r['content']}")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
def get_deviations(project_id):
|
||||||
|
d = latest_artifact(project_id, "deviations")
|
||||||
|
return d.get("items", []) if d else []
|
||||||
|
|
||||||
|
def add_deviation(project_id, item):
|
||||||
|
items = get_deviations(project_id)
|
||||||
|
item["at"] = now()
|
||||||
|
items.append(item)
|
||||||
|
save_artifact(project_id, "deviations", {"items": items})
|
||||||
|
|
||||||
|
def stage_artifact_context(project_id, stage):
|
||||||
|
"""Текущий артефакт этапа + уже зафиксированные отклонения — как контекст для Елены."""
|
||||||
|
s = (stage or "").lower()
|
||||||
|
parts = []
|
||||||
|
if s in ("4", "canvas", "idef0", "analysis", "model", "анализ"):
|
||||||
|
cv = latest_artifact(project_id, "canvas")
|
||||||
|
if cv:
|
||||||
|
parts.append("ТЕКУЩАЯ СТРАТЕГИЯ (Business Model Canvas):\n" + json.dumps(cv, ensure_ascii=False)[:4000])
|
||||||
|
mrow = db().execute(
|
||||||
|
"SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (project_id,)
|
||||||
|
).fetchone()
|
||||||
|
if mrow:
|
||||||
|
parts.append("ТЕКУЩАЯ ФУНКЦИОНАЛЬНАЯ МОДЕЛЬ (IDEF0):\n" + mrow["blocks_json"][:4000])
|
||||||
|
elif s in ("5", "spec", "план", "тз"):
|
||||||
|
sp = latest_artifact(project_id, "spec")
|
||||||
|
if sp:
|
||||||
|
parts.append("ТЕКУЩЕЕ ТЗ:\n" + json.dumps(sp, ensure_ascii=False)[:5000])
|
||||||
|
dev = get_deviations(project_id)
|
||||||
|
if dev:
|
||||||
|
parts.append("УЖЕ ЗАФИКСИРОВАННЫЕ ОТКЛОНЕНИЯ КЛИЕНТА (не дублируй их):\n" + json.dumps(dev, ensure_ascii=False)[:2000])
|
||||||
|
return "\n\n".join(parts) if parts else None
|
||||||
|
|
||||||
def save_artifact(project_id, kind, data):
|
def save_artifact(project_id, kind, data):
|
||||||
con = db()
|
con = db()
|
||||||
con.execute(
|
con.execute(
|
||||||
@ -306,6 +355,112 @@ def chat():
|
|||||||
con.commit()
|
con.commit()
|
||||||
return jsonify({"reply": reply, "usage": usage})
|
return jsonify({"reply": reply, "usage": usage})
|
||||||
|
|
||||||
|
# ── «Спросить Елену» на этапах 3–5: Q&A с полной памятью + фиксация отклонений ──
|
||||||
|
ASK_GUIDE = """РЕЖИМ ВОПРОСОВ ПО ЭТАПУ.
|
||||||
|
Клиент задаёт вопросы по уже построенному артефакту (стратегия / функциональная модель / ТЗ). Правила:
|
||||||
|
1. Отвечай как Елена — простым языком, по делу, без воды. У тебя есть полный контекст: интервью, документы, текущий артефакт.
|
||||||
|
2. Если клиент НЕ согласен и настаивает на своём («мне так удобно», «у нас так нельзя») — не переубеждай силой. Вызови инструмент record_deviation: что рекомендуешь ты (методологически верно), что выбрал клиент и ПОЧЕМУ (причина клиента — самое важное, часто это реальное ограничение, не учтённое в интервью).
|
||||||
|
3. Сам артефакт в этом режиме ты НЕ перестраиваешь — только объясняешь и фиксируешь отклонения. Перестройку сделает консультант.
|
||||||
|
4. После фиксации отклонения — коротко подтверди клиенту, что его пожелание записано и будет учтено при внедрении, но честно оставь свою рекомендацию в силе."""
|
||||||
|
|
||||||
|
DEVIATION_TOOL = {
|
||||||
|
"name": "record_deviation",
|
||||||
|
"description": "Зафиксировать отклонение: клиент настоял на варианте, отличном от методологически верного. Вызывай ТОЛЬКО когда клиент явно отказывается от рекомендации и выбирает своё.",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"stage": {"type": "string", "description": "Этап/артефакт: canvas, idef0, spec, documents"},
|
||||||
|
"node": {"type": "string", "description": "Конкретный узел/блок/функция, к которому относится отклонение"},
|
||||||
|
"elena_rec": {"type": "string", "description": "Что рекомендует Елена — методологически верно"},
|
||||||
|
"client_choice": {"type": "string", "description": "Что выбрал клиент"},
|
||||||
|
"reason": {"type": "string", "description": "Причина клиента — почему ему так нужно/удобно (самое важное)"}
|
||||||
|
},
|
||||||
|
"required": ["node", "elena_rec", "client_choice", "reason"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_ask(messages, system, pid):
|
||||||
|
"""Агентный цикл: Елена отвечает и при необходимости фиксирует отклонение через tool."""
|
||||||
|
recorded = False
|
||||||
|
for _ in range(3):
|
||||||
|
resp = client.messages.create(
|
||||||
|
model=MODEL, max_tokens=1100,
|
||||||
|
system=[{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}],
|
||||||
|
tools=[DEVIATION_TOOL],
|
||||||
|
messages=messages
|
||||||
|
)
|
||||||
|
tool_uses = [b for b in resp.content if b.type == "tool_use"]
|
||||||
|
text = "".join(b.text for b in resp.content if b.type == "text")
|
||||||
|
if tool_uses:
|
||||||
|
messages.append({"role": "assistant", "content": resp.content})
|
||||||
|
results = []
|
||||||
|
for tu in tool_uses:
|
||||||
|
if tu.name == "record_deviation":
|
||||||
|
item = dict(tu.input)
|
||||||
|
item.setdefault("stage", "")
|
||||||
|
add_deviation(pid, item)
|
||||||
|
recorded = True
|
||||||
|
results.append({"type": "tool_result", "tool_use_id": tu.id, "content": "Отклонение зафиксировано."})
|
||||||
|
messages.append({"role": "user", "content": results})
|
||||||
|
continue
|
||||||
|
return (text or "Записала ваши пожелания."), recorded
|
||||||
|
return "Записала ваши пожелания — учтём при внедрении.", recorded
|
||||||
|
|
||||||
|
@app.route("/api/ask", methods=["POST"])
|
||||||
|
def ask():
|
||||||
|
data = request.get_json(force=True) or {}
|
||||||
|
token = data.get("token")
|
||||||
|
msg = (data.get("message") or "").strip()
|
||||||
|
stage = (data.get("stage") or "").strip()
|
||||||
|
if not token or not msg:
|
||||||
|
return jsonify({"error": "token and message required"}), 400
|
||||||
|
proj = get_project(token)
|
||||||
|
if not proj:
|
||||||
|
return jsonify({"error": "project not found"}), 404
|
||||||
|
pid = proj["id"]
|
||||||
|
|
||||||
|
con = db()
|
||||||
|
con.execute(
|
||||||
|
"INSERT INTO messages (project_id, role, content, created_at, channel) VALUES (?,?,?,?,?)",
|
||||||
|
(pid, "user", msg, now(), "qa")
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
|
||||||
|
# Полный контекст проекта — одним блоком, чтобы не ломать чередование ролей
|
||||||
|
ctx_parts = []
|
||||||
|
iv = interview_as_text(pid)
|
||||||
|
if iv:
|
||||||
|
ctx_parts.append(iv)
|
||||||
|
docs = documents_context(pid)
|
||||||
|
if docs:
|
||||||
|
ctx_parts.append(docs)
|
||||||
|
art = stage_artifact_context(pid, stage)
|
||||||
|
if art:
|
||||||
|
ctx_parts.append(art)
|
||||||
|
context_block = "\n\n".join(ctx_parts)
|
||||||
|
|
||||||
|
qa = [{"role": m["role"], "content": m["content"]} for m in db().execute(
|
||||||
|
"SELECT role, content FROM messages WHERE project_id=? AND channel='qa' ORDER BY id", (pid,)
|
||||||
|
).fetchall()]
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": (context_block or "Контекст проекта пока пуст.")
|
||||||
|
+ "\n\n[Ниже — вопросы клиента по текущему этапу. Отвечай как Елена.]"},
|
||||||
|
{"role": "assistant", "content": "Держу весь контекст проекта. Готова ответить на ваши вопросы по этапу."},
|
||||||
|
] + qa
|
||||||
|
|
||||||
|
try:
|
||||||
|
reply, recorded = run_ask(messages, SYSTEM_PROMPT + "\n\n" + ASK_GUIDE, pid)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
con.execute(
|
||||||
|
"INSERT INTO messages (project_id, role, content, created_at, channel) VALUES (?,?,?,?,?)",
|
||||||
|
(pid, "assistant", reply, now(), "qa")
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
return jsonify({"reply": reply, "deviation_recorded": recorded})
|
||||||
|
|
||||||
# ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ──
|
# ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ──
|
||||||
ARROW = {
|
ARROW = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -1164,7 +1319,7 @@ def get_project_state(token):
|
|||||||
if not proj:
|
if not proj:
|
||||||
return jsonify({"error": "not found"}), 404
|
return jsonify({"error": "not found"}), 404
|
||||||
msgs = db().execute(
|
msgs = db().execute(
|
||||||
"SELECT role, content, created_at FROM messages WHERE project_id=? ORDER BY id", (proj["id"],)
|
"SELECT role, content, created_at FROM messages WHERE project_id=? AND (channel='interview' OR channel IS NULL) ORDER BY id", (proj["id"],)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
model_row = db().execute(
|
model_row = db().execute(
|
||||||
"SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)
|
"SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)
|
||||||
@ -1200,7 +1355,9 @@ def get_project_state(token):
|
|||||||
"tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []),
|
"tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []),
|
||||||
"pricing": latest_artifact(proj["id"], "pricing"),
|
"pricing": latest_artifact(proj["id"], "pricing"),
|
||||||
"signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None,
|
"signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None,
|
||||||
"documents": [json.loads(r["data_json"]) and {"filename": json.loads(r["data_json"])["filename"], "size": json.loads(r["data_json"]).get("size",0)} for r in db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (proj["id"],)).fetchall()]
|
"documents": [json.loads(r["data_json"]) and {"filename": json.loads(r["data_json"])["filename"], "size": json.loads(r["data_json"]).get("size",0)} for r in db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (proj["id"],)).fetchall()],
|
||||||
|
"qa": [{"role": m["role"], "content": m["content"]} for m in db().execute("SELECT role, content FROM messages WHERE project_id=? AND channel='qa' ORDER BY id", (proj["id"],)).fetchall()],
|
||||||
|
"deviations": get_deviations(proj["id"])
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Telegram Bot ─────────────────────────────────────
|
# ── Telegram Bot ─────────────────────────────────────
|
||||||
|
|||||||
@ -66,6 +66,28 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
|
|||||||
.scroll{flex:1;overflow-y:auto}
|
.scroll{flex:1;overflow-y:auto}
|
||||||
/* Chat */
|
/* Chat */
|
||||||
.chat{padding:24px 26px;display:flex;flex-direction:column;gap:14px}
|
.chat{padding:24px 26px;display:flex;flex-direction:column;gap:14px}
|
||||||
|
/* Спросить Елену — док на этапах 3-5 */
|
||||||
|
.askdock{display:none;border-top:1px solid var(--border);background:var(--white);flex:0 0 auto}
|
||||||
|
.askdock.show{display:block}
|
||||||
|
.askdock-head{display:flex;align-items:center;gap:8px;padding:11px 18px;cursor:pointer;font-size:13px;font-weight:700;color:var(--primary);user-select:none}
|
||||||
|
.askdock-head .ad-sub{font-weight:500;color:#9ca3af;font-size:12px}
|
||||||
|
.askdock-head .ad-chev{margin-left:auto;transition:transform .2s}
|
||||||
|
.askdock.open .ad-chev{transform:rotate(180deg)}
|
||||||
|
.askdock-body{display:none;border-top:1px solid var(--border)}
|
||||||
|
.askdock.open .askdock-body{display:block}
|
||||||
|
.askdock-thread{max-height:240px;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:10px}
|
||||||
|
.askdock-thread:empty{display:none}
|
||||||
|
.askdock-inbar{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--border)}
|
||||||
|
.askdock-inbar textarea{flex:1;border:1px solid var(--border);border-radius:10px;padding:9px 12px;font:inherit;font-size:13px;resize:none;max-height:90px;outline:none}
|
||||||
|
.askdock-inbar textarea:focus{border-color:var(--primary)}
|
||||||
|
.am{display:flex;gap:8px;font-size:13px;line-height:1.45}
|
||||||
|
.am .am-av{width:24px;height:24px;border-radius:50%;flex:0 0 24px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff}
|
||||||
|
.am.u{flex-direction:row-reverse}
|
||||||
|
.am.u .am-av{background:#64748b}
|
||||||
|
.am.e .am-av{background:var(--primary)}
|
||||||
|
.am .am-bb{background:#f1f5f9;border-radius:10px;padding:7px 11px;max-width:86%}
|
||||||
|
.am.u .am-bb{background:#e2e8f0}
|
||||||
|
.am-dev{font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:4px 8px;margin-top:5px;font-weight:600}
|
||||||
.msg{display:flex;gap:10px;max-width:80%}
|
.msg{display:flex;gap:10px;max-width:80%}
|
||||||
.msg.user{align-self:flex-end;flex-direction:row-reverse}
|
.msg.user{align-self:flex-end;flex-direction:row-reverse}
|
||||||
.av{width:30px;height:30px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#fff}
|
.av{width:30px;height:30px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;color:#fff}
|
||||||
@ -324,6 +346,18 @@ body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);displ
|
|||||||
<div class="scroll"><div class="pad" id="specPad"></div></div>
|
<div class="scroll"><div class="pad" id="specPad"></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Спросить Елену — док на этапах 3-5 -->
|
||||||
|
<div class="askdock" id="askDock">
|
||||||
|
<div class="askdock-head" onclick="toggleAsk()">💬 Спросить Елену <span class="ad-sub" id="adSub">об этом этапе</span><svg class="ad-chev" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg></div>
|
||||||
|
<div class="askdock-body">
|
||||||
|
<div class="askdock-thread" id="askThread"></div>
|
||||||
|
<div class="askdock-inbar">
|
||||||
|
<textarea id="askInp" rows="1" placeholder="Спросите Елену об этом этапе…" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();askElena()}"></textarea>
|
||||||
|
<button class="icon-btn send" id="askSend" onclick="askElena()"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -358,6 +392,13 @@ function go(n){
|
|||||||
if(n===3)renderDocs();
|
if(n===3)renderDocs();
|
||||||
if(n===4)renderAnalysis();
|
if(n===4)renderAnalysis();
|
||||||
if(n===5)renderSpecPane();
|
if(n===5)renderSpecPane();
|
||||||
|
// Док «Спросить Елену» — только на этапах 3-5
|
||||||
|
const dock=document.getElementById("askDock");
|
||||||
|
if(dock){
|
||||||
|
if(n>=3&&n<=5){dock.classList.add("show");document.getElementById("adSub").textContent=STAGE_LBL[n]||"об этапе";
|
||||||
|
if(!dock.dataset.rendered){renderAskThread();dock.dataset.rendered="1";}}
|
||||||
|
else dock.classList.remove("show");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProfile(){
|
async function saveProfile(){
|
||||||
@ -394,6 +435,37 @@ function addMsg(role,text){const m=document.createElement("div");m.className="ms
|
|||||||
function showTyping(){const t=document.createElement("div");t.className="msg";t.id="typing";t.innerHTML=`<div class="av e">Е</div><div class="typing"><span></span><span></span><span></span></div>`;chat.appendChild(t);chat.scrollTop=chat.scrollHeight}
|
function showTyping(){const t=document.createElement("div");t.className="msg";t.id="typing";t.innerHTML=`<div class="av e">Е</div><div class="typing"><span></span><span></span><span></span></div>`;chat.appendChild(t);chat.scrollTop=chat.scrollHeight}
|
||||||
function hideTyping(){const t=document.getElementById("typing");if(t)t.remove()}
|
function hideTyping(){const t=document.getElementById("typing");if(t)t.remove()}
|
||||||
|
|
||||||
|
/* ── Спросить Елену (этапы 3-5) ── */
|
||||||
|
const STAGE_LBL={3:"о документах",4:"о стратегии и модели",5:"о ТЗ и плане"};
|
||||||
|
function toggleAsk(){document.getElementById("askDock").classList.toggle("open")}
|
||||||
|
function addAsk(role,text,dev){
|
||||||
|
const t=document.getElementById("askThread");if(!t)return;
|
||||||
|
const m=document.createElement("div");m.className="am "+(role==="user"?"u":"e");
|
||||||
|
m.innerHTML=`<div class="am-av">${role==="user"?"Я":"Е"}</div><div class="am-bb">${fmt(text)}${dev?'<div class="am-dev">⚠ Ваше пожелание зафиксировано — учтём при внедрении</div>':''}</div>`;
|
||||||
|
t.appendChild(m);t.scrollTop=t.scrollHeight;
|
||||||
|
}
|
||||||
|
function renderAskThread(){
|
||||||
|
const t=document.getElementById("askThread");if(!t)return;
|
||||||
|
t.innerHTML="";(state&&state.qa||[]).forEach(m=>addAsk(m.role==="user"?"user":"elena",m.content));
|
||||||
|
}
|
||||||
|
async function askElena(){
|
||||||
|
const inp=document.getElementById("askInp");const text=inp.value.trim();if(!text)return;
|
||||||
|
inp.value="";inp.style.height="auto";
|
||||||
|
document.getElementById("askDock").classList.add("open");
|
||||||
|
addAsk("user",text);
|
||||||
|
const btn=document.getElementById("askSend");btn.disabled=true;
|
||||||
|
const tp=document.createElement("div");tp.className="am e";tp.id="askTyping";tp.innerHTML='<div class="am-av">Е</div><div class="am-bb">…</div>';
|
||||||
|
document.getElementById("askThread").appendChild(tp);
|
||||||
|
try{
|
||||||
|
const r=await fetch(`${API}/api/ask`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token,message:text,stage:String(cur)})});
|
||||||
|
const d=await r.json();
|
||||||
|
const x=document.getElementById("askTyping");if(x)x.remove();
|
||||||
|
addAsk("elena",d.reply||("Ошибка: "+(d.error||"?")),d.deviation_recorded);
|
||||||
|
state.qa=state.qa||[];state.qa.push({role:"user",content:text},{role:"assistant",content:d.reply||""});
|
||||||
|
}catch(e){const x=document.getElementById("askTyping");if(x)x.remove();addAsk("elena","Ошибка связи: "+e.message)}
|
||||||
|
btn.disabled=false;
|
||||||
|
}
|
||||||
|
|
||||||
async function init(){
|
async function init(){
|
||||||
// Telegram Mini App: развернуть на весь экран + токен из start_param если нет в URL
|
// Telegram Mini App: развернуть на весь экран + токен из start_param если нет в URL
|
||||||
const tg=window.Telegram&&window.Telegram.WebApp;
|
const tg=window.Telegram&&window.Telegram.WebApp;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user