mirror of
https://github.com/wasrusgen/zashita-brandbook.git
synced 2026-06-03 15:04:49 +00:00
feat: persistent memory - chat history, contract storage, dossier compression
This commit is contained in:
parent
cafe84c57f
commit
a651285284
169
mockup.html
169
mockup.html
@ -2746,6 +2746,8 @@ function startScan() {
|
|||||||
});
|
});
|
||||||
// Сохраняем в localStorage — переживёт перезагрузку страницы
|
// Сохраняем в localStorage — переживёт перезагрузку страницы
|
||||||
try { localStorage.setItem('zashita_deadlines', JSON.stringify(_DEADLINES)); } catch(e){}
|
try { localStorage.setItem('zashita_deadlines', JSON.stringify(_DEADLINES)); } catch(e){}
|
||||||
|
// Сохраняем договор в хранилище (persistent memory)
|
||||||
|
_saveContract(data.meta, data.risks, data.deadlines, text);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function(e){ console.warn('API /deadlines:', e); });
|
.catch(function(e){ console.warn('API /deadlines:', e); });
|
||||||
@ -4527,37 +4529,134 @@ var _apiAvailable = null; // null=не проверяли, true/false
|
|||||||
|
|
||||||
// Текущий контекст договора (deadlines из последнего анализа)
|
// Текущий контекст договора (deadlines из последнего анализа)
|
||||||
var _contractDeadlines = null;
|
var _contractDeadlines = null;
|
||||||
var _chatHistory = []; // история чата для /api/elena
|
|
||||||
|
|
||||||
// Собирает контекст клиента для передачи Елене
|
// ── PERSISTENT MEMORY ──────────────────────────────────────────────────────────
|
||||||
|
var _MEM_HISTORY = 'zashita_chat_v1'; // история сообщений
|
||||||
|
var _MEM_CONTRACTS = 'zashita_contracts_v1'; // хранилище договоров
|
||||||
|
var _MEM_DOSSIER = 'zashita_dossier_v1'; // сжатое досье компании
|
||||||
|
|
||||||
|
// История чата — загружается из localStorage, переживает перезагрузку
|
||||||
|
var _chatHistory = (function() {
|
||||||
|
try { var h = localStorage.getItem(_MEM_HISTORY); return h ? JSON.parse(h) : []; }
|
||||||
|
catch(e) { return []; }
|
||||||
|
})();
|
||||||
|
|
||||||
|
function _saveHistory() {
|
||||||
|
try { localStorage.setItem(_MEM_HISTORY, JSON.stringify(_chatHistory.slice(-60))); }
|
||||||
|
catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хранилище договоров
|
||||||
|
function _saveContract(meta, risks, deadlines, textPreview) {
|
||||||
|
try {
|
||||||
|
var contracts = JSON.parse(localStorage.getItem(_MEM_CONTRACTS) || '[]');
|
||||||
|
var c = {
|
||||||
|
id: Date.now(),
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
type: (meta && meta.type) || 'Договор',
|
||||||
|
counterparty: (meta && (meta.counterparty || meta.party_b || meta.contractor)) || '',
|
||||||
|
start: (meta && meta.start) || '',
|
||||||
|
end: (meta && meta.end) || '',
|
||||||
|
deadlines_count: (deadlines || []).length,
|
||||||
|
risks_critical: (risks || []).filter(function(r){ return r.level === 'critical'; }).length,
|
||||||
|
top_risks: (risks || []).slice(0,3).map(function(r){ return r.title; }),
|
||||||
|
preview: (textPreview || '').slice(0, 150)
|
||||||
|
};
|
||||||
|
// Дедупликация по типу+контрагенту
|
||||||
|
contracts = contracts.filter(function(x){
|
||||||
|
return !(x.type === c.type && x.counterparty === c.counterparty);
|
||||||
|
});
|
||||||
|
contracts.unshift(c);
|
||||||
|
localStorage.setItem(_MEM_CONTRACTS, JSON.stringify(contracts.slice(0, 15)));
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getContracts() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(_MEM_CONTRACTS) || '[]'); }
|
||||||
|
catch(e) { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Досье — структурированные факты о клиенте
|
||||||
|
function _getDossier() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(_MEM_DOSSIER) || 'null'); }
|
||||||
|
catch(e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateDossier(patch) {
|
||||||
|
// patch: {facts:[], decisions:[], open:[]}
|
||||||
|
try {
|
||||||
|
var d = _getDossier() || { facts:[], decisions:[], open:[], updated:'' };
|
||||||
|
if (patch.facts) d.facts = (d.facts.concat(patch.facts)).slice(-20);
|
||||||
|
if (patch.decisions) d.decisions = (d.decisions.concat(patch.decisions)).slice(-10);
|
||||||
|
if (patch.open) d.open = patch.open; // заменяем открытые вопросы
|
||||||
|
d.updated = new Date().toISOString();
|
||||||
|
localStorage.setItem(_MEM_DOSSIER, JSON.stringify(d));
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем факты из ответа Елены (без LLM — простые эвристики)
|
||||||
|
function _extractFacts(userMsg, elenaReply) {
|
||||||
|
var facts = [], decisions = [], open = [];
|
||||||
|
var u = (userMsg||'').toLowerCase(), e = (elenaReply||'').toLowerCase();
|
||||||
|
if (/уже отправил|отправили|подписал|оплатил|сделал/.test(u))
|
||||||
|
decisions.push(userMsg.slice(0,80));
|
||||||
|
if (/не успею|не знаю|непонятно|что делать/.test(u))
|
||||||
|
open.push(userMsg.slice(0,80));
|
||||||
|
return { facts: facts, decisions: decisions, open: open };
|
||||||
|
}
|
||||||
|
// ───────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Собирает контекст клиента для передачи Елене (включая persistent memory)
|
||||||
function _buildElenaContext() {
|
function _buildElenaContext() {
|
||||||
var clientName = '';
|
var clientName = '';
|
||||||
var caseContext = '';
|
|
||||||
try {
|
try {
|
||||||
// Имя из localStorage (B2B или физлицо)
|
|
||||||
var b2b = JSON.parse(localStorage.getItem('zashita_b2b') || 'null');
|
var b2b = JSON.parse(localStorage.getItem('zashita_b2b') || 'null');
|
||||||
if (b2b && b2b.name) clientName = b2b.name;
|
if (b2b && b2b.name) clientName = b2b.name;
|
||||||
// Имя из stats (если физлицо)
|
|
||||||
if (!clientName) {
|
if (!clientName) {
|
||||||
var stats = JSON.parse(localStorage.getItem('zashita_intake_stats') || '[]');
|
var stats = JSON.parse(localStorage.getItem('zashita_intake_stats') || '[]');
|
||||||
if (stats.length && stats[0].name) clientName = stats[0].name;
|
if (stats.length && stats[0].name) clientName = stats[0].name;
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
||||||
// Контекст дела — ТОЛЬКО если пользователь реально загрузил договор в этой сессии
|
var parts = [];
|
||||||
// НЕ берём из demo-дедлайнов/_rcLastContext чтобы не путать Елену чужим контекстом
|
|
||||||
var contractText = (document.getElementById('el-paste') || {}).value || '';
|
// 1. Хранилище договоров — Елена знает все договоры клиента
|
||||||
if (contractText && contractText.length > 50) {
|
var contracts = _getContracts();
|
||||||
// Есть реальный текст договора — используем первые 300 символов как контекст
|
if (contracts.length) {
|
||||||
caseContext = contractText.slice(0, 300);
|
var cList = contracts.map(function(c) {
|
||||||
} else if (_rcLastContext && _rcLastContext.caseName && _rcLastContext._userLoaded) {
|
var s = c.type;
|
||||||
// _userLoaded = флаг что пользователь сам загрузил, не demo
|
if (c.counterparty) s += ' с ' + c.counterparty;
|
||||||
caseContext = _rcLastContext.caseName;
|
if (c.risks_critical) s += ' (' + c.risks_critical + ' крит. риска)';
|
||||||
|
return s;
|
||||||
|
}).join('; ');
|
||||||
|
parts.push('Договоры клиента: ' + cList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Активные сроки
|
||||||
|
var activeDL = (_DEADLINES || []).filter(function(d){ return !d.done; });
|
||||||
|
if (activeDL.length) {
|
||||||
|
parts.push('Активные сроки: ' + activeDL.slice(0,4).map(function(d){
|
||||||
|
return '«' + d.title + '» ' + d.date;
|
||||||
|
}).join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Досье — факты и решения из прошлых сессий
|
||||||
|
var dossier = _getDossier();
|
||||||
|
if (dossier) {
|
||||||
|
if (dossier.decisions && dossier.decisions.length)
|
||||||
|
parts.push('Принятые решения: ' + dossier.decisions.slice(-3).join('; '));
|
||||||
|
if (dossier.open && dossier.open.length)
|
||||||
|
parts.push('Открытые вопросы: ' + dossier.open.slice(0,2).join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Текст загруженного договора (текущая сессия)
|
||||||
|
var contractText = (document.getElementById('el-paste') || {}).value || '';
|
||||||
|
if (contractText && contractText.length > 50)
|
||||||
|
parts.push('Текущий договор (загружен): ' + contractText.slice(0, 300));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
client_name: clientName,
|
client_name: clientName,
|
||||||
case_context: caseContext,
|
case_context: parts.join('\n'),
|
||||||
parties: _postalData ? {
|
parties: _postalData ? {
|
||||||
counterparty: _postalData.counterparty,
|
counterparty: _postalData.counterparty,
|
||||||
counterEmail: _postalData.counterEmail
|
counterEmail: _postalData.counterEmail
|
||||||
@ -4638,8 +4737,12 @@ function _elenaApi(txt, intent, callback) {
|
|||||||
var parsed = _parseElenaActions(d.reply);
|
var parsed = _parseElenaActions(d.reply);
|
||||||
_chatHistory.push({role:'user', content: txt});
|
_chatHistory.push({role:'user', content: txt});
|
||||||
_chatHistory.push({role:'assistant', content: parsed.text});
|
_chatHistory.push({role:'assistant', content: parsed.text});
|
||||||
// Передаём в callback объект {text, actions} — совместимо со старым кодом
|
// Сохраняем историю в localStorage после каждого обмена
|
||||||
// (старый код ожидает строку — если передать строку, ничего не сломается)
|
_saveHistory();
|
||||||
|
// Обновляем досье фактами из этого обмена
|
||||||
|
var facts = _extractFacts(txt, parsed.text);
|
||||||
|
if (facts.decisions.length || facts.open.length)
|
||||||
|
_updateDossier(facts);
|
||||||
callback(parsed.text, parsed.actions);
|
callback(parsed.text, parsed.actions);
|
||||||
} else {
|
} else {
|
||||||
callback(null, []);
|
callback(null, []);
|
||||||
@ -6431,4 +6534,36 @@ function tab(name){
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── COMPRESS SESSION → DOSSIER ─────────────────────────────────────────────────
|
||||||
|
document.addEventListener('visibilitychange', function() {
|
||||||
|
if (document.visibilityState === 'hidden') _compressAsync(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
function _compressAsync(showToast) {
|
||||||
|
if (!_apiAvailable || _chatHistory.length < 4) return;
|
||||||
|
fetch(API_BASE + '/api/compress', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
history: _chatHistory.slice(-30),
|
||||||
|
existing_dossier: _getDossier() || {}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(d){
|
||||||
|
if (d.facts || d.decisions || d.open) {
|
||||||
|
_updateDossier(d);
|
||||||
|
if (showToast) toast('💾 Елена запомнила контекст');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(){});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Флаг памяти — используется для контекстного приветствия
|
||||||
|
window._hasMemory = (function() {
|
||||||
|
var h = _chatHistory.length, c = _getContracts().length;
|
||||||
|
return (h || c) ? { messages: h, contracts: c, hasDossier: !!_getDossier() } : null;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user