feat: shared AI cache via git (ai-cache.json)
- AI analysis loaded from git on startup (same for all users) - Saving new response commits ai-cache.json via Gitea API - Requires GITEA_TOKEN in config.js to update cache - Update UI: auto-expanded result, collapsible refresh section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85f344eae6
commit
40882b62ea
5
ai-cache.json
Normal file
5
ai-cache.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"generated_at": null,
|
||||
"period": null,
|
||||
"response": null
|
||||
}
|
||||
126
dashboard.html
126
dashboard.html
@ -159,6 +159,15 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
|
||||
.ai-divider { height: 1px; background: var(--color-border); margin: 12px 0; }
|
||||
.ai-hint { font-size: 11px; color: var(--color-text-secondary); text-align: center; line-height: 1.5; }
|
||||
.ai-key-note { font-size: 11px; color: var(--color-green); text-align: center; margin-bottom: 8px; font-weight: 500; }
|
||||
.ai-cache-meta { display: flex; align-items: center; gap: 4px; margin-bottom: 6px; }
|
||||
.ai-update-details { margin-top: 4px; }
|
||||
.ai-update-details[open] .ai-update-summary { color: var(--color-brand); }
|
||||
.ai-update-summary {
|
||||
font-size: 13px; font-weight: 600; color: var(--color-text-secondary);
|
||||
cursor: pointer; list-style: none; padding: 6px 0; user-select: none;
|
||||
}
|
||||
.ai-update-summary::-webkit-details-marker { display: none; }
|
||||
.ai-update-summary:hover { color: var(--color-brand); }
|
||||
|
||||
/* ── RESPONSIVE ── */
|
||||
@media (max-width: 1200px) { .summary-bar { grid-template-columns: repeat(3,1fr); } }
|
||||
@ -271,16 +280,30 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
|
||||
<!-- AI Panel -->
|
||||
<div class="ai-panel">
|
||||
<div class="ai-panel-title">🤖 AI-анализ KPI</div>
|
||||
<div class="ai-api-row">
|
||||
<div id="ai-key-note" class="ai-key-note hidden">✓ Токен загружен из config.js</div>
|
||||
<input type="password" class="api-key-input" id="ai-api-key" placeholder="Введите DeepSeek API ключ">
|
||||
<button class="btn-primary" id="btn-ai-analyze">Запустить анализ</button>
|
||||
|
||||
<!-- Cached response (shown to all users automatically) -->
|
||||
<div class="ai-loading hidden" id="ai-loading"><div class="spinner"></div><span>Получаем анализ...</span></div>
|
||||
<div class="ai-result hidden" id="ai-result">
|
||||
<div class="ai-cache-meta">
|
||||
<span id="ai-cache-date" style="font-size:11px;color:var(--color-text-secondary);">Загрузка...</span>
|
||||
<span style="font-size:11px;color:var(--color-text-secondary);">· общий для всех</span>
|
||||
</div>
|
||||
<div id="ai-text"></div>
|
||||
<button class="btn-copy" id="btn-ai-copy">📋 Скопировать</button>
|
||||
</div>
|
||||
<div class="ai-loading hidden" id="ai-loading"><div class="spinner"></div><span>Анализируем данные...</span></div>
|
||||
<div class="ai-result hidden" id="ai-result"><div id="ai-text"></div><button class="btn-copy" id="btn-ai-copy">📋 Скопировать</button></div>
|
||||
<div class="ai-error hidden" id="ai-error"></div>
|
||||
|
||||
<!-- Update section: only useful if you have DeepSeek key -->
|
||||
<div class="ai-divider"></div>
|
||||
<p class="ai-hint">DeepSeek API · Ключ не сохраняется за пределами сессии браузера</p>
|
||||
<details class="ai-update-details">
|
||||
<summary class="ai-update-summary">🔄 Обновить анализ</summary>
|
||||
<div class="ai-api-row" style="margin-top:10px;">
|
||||
<div id="ai-key-note" class="ai-key-note hidden">✓ DeepSeek токен загружен из config.js</div>
|
||||
<input type="password" class="api-key-input" id="ai-api-key" placeholder="DeepSeek API ключ">
|
||||
<button class="btn-primary" id="btn-ai-analyze">Запустить и сохранить для всех</button>
|
||||
</div>
|
||||
<p class="ai-hint" style="margin-top:8px;">Новый ответ сохранится в репо и сразу станет доступен всем пользователям. Обновляйте раз в месяц при новых данных.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -444,7 +467,12 @@ const EMBEDDED_CSV = `"report_period_id";"entry_date";"abons";"registered_total"
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const MONTH_NAMES = ['','Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
|
||||
const CSV_FILENAME = 'drb_iliyas_kpi_2026.csv';
|
||||
const CSV_FILENAME = 'drb_iliyas_kpi_2026.csv';
|
||||
const GITEA_API = 'https://git.telecom.quest/api/v1';
|
||||
const GITEA_OWNER = 'Kyrykbaev-I';
|
||||
const GITEA_REPO = 'kpi-dashboard';
|
||||
const AI_CACHE_FILE = 'ai-cache.json';
|
||||
const AI_CACHE_RAW = `https://git.telecom.quest/${GITEA_OWNER}/${GITEA_REPO}/raw/branch/main/${AI_CACHE_FILE}`;
|
||||
|
||||
const AppState = {
|
||||
rawRows: [], snapshots: [], dailySeries: {},
|
||||
@ -696,7 +724,7 @@ function showDashboard() {
|
||||
document.getElementById('dashboard').style.display = 'block';
|
||||
renderHeader(); renderSummaryBar(); renderToolbar();
|
||||
destroyAllCharts(); renderAllCharts();
|
||||
restoreAiCache(); initAiKey();
|
||||
loadAiCache(); initAiKey();
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
@ -1120,6 +1148,7 @@ async function runAiAnalysis() {
|
||||
AppState.lastAiResponse=text;
|
||||
sessionStorage.setItem('lastAiResponse',text);
|
||||
showAiResult(text);
|
||||
await saveAiCache(text); // save to git repo for all users (requires GITEA_TOKEN in config.js)
|
||||
} catch(e) {
|
||||
clearTimeout(t);
|
||||
showAiError(e.name==='AbortError'?'Превышено время ожидания. Попробуйте снова.':'Ошибка: '+e.message);
|
||||
@ -1131,6 +1160,85 @@ function showAiResult(text){document.getElementById('ai-text').textContent=text;
|
||||
function showAiError(msg){const el=document.getElementById('ai-error');if(msg){el.textContent=msg;el.classList.remove('hidden');}else el.classList.add('hidden');}
|
||||
function restoreAiCache(){const c=sessionStorage.getItem('lastAiResponse');if(c)showAiResult(c);}
|
||||
|
||||
// ═══════════════════ AI CACHE (git-based, shared for all users) ═══════════════════
|
||||
|
||||
/** Load AI response from ai-cache.json stored in the git repo */
|
||||
async function loadAiCache() {
|
||||
const dateEl = document.getElementById('ai-cache-date');
|
||||
try {
|
||||
const resp = await fetch(AI_CACHE_RAW, {cache:'no-cache'});
|
||||
if (!resp.ok) { if(dateEl) dateEl.textContent = 'Кэш не найден'; return; }
|
||||
const data = await resp.json();
|
||||
if (!data.response) return;
|
||||
AppState.lastAiResponse = data.response;
|
||||
showAiResult(data.response);
|
||||
if (dateEl && data.generated_at) {
|
||||
const d = new Date(data.generated_at);
|
||||
dateEl.textContent = 'Обновлено: ' + d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
|
||||
}
|
||||
} catch(e) {
|
||||
if(dateEl) dateEl.textContent = 'Кэш недоступен';
|
||||
console.info('AI cache not loaded:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Save AI response to ai-cache.json in git via Gitea API.
|
||||
* Requires window.GITEA_TOKEN (from config.js). */
|
||||
async function saveAiCache(text) {
|
||||
const token = window.GITEA_TOKEN;
|
||||
if (!token) return; // no write access — cache stays local only
|
||||
|
||||
const lastSnap = AppState.snapshots[AppState.snapshots.length-1];
|
||||
const payload = {
|
||||
generated_at: new Date().toISOString(),
|
||||
period: periodLabel(lastSnap.report_period_id),
|
||||
response: text,
|
||||
};
|
||||
|
||||
// base64-encode the JSON (Gitea API requires base64 content)
|
||||
const content = btoa(unescape(encodeURIComponent(JSON.stringify(payload, null, 2))));
|
||||
|
||||
// Fetch current file SHA (needed for update; absent for first create)
|
||||
let sha = null;
|
||||
try {
|
||||
const fileResp = await fetch(
|
||||
`${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
|
||||
{headers:{'Authorization':`token ${token}`}}
|
||||
);
|
||||
if (fileResp.ok) { sha = (await fileResp.json()).sha; }
|
||||
} catch {}
|
||||
|
||||
const body = JSON.stringify({
|
||||
message: `Update AI analysis cache — ${payload.period}`,
|
||||
content,
|
||||
...(sha ? {sha} : {}),
|
||||
});
|
||||
|
||||
try {
|
||||
const saveResp = await fetch(
|
||||
`${GITEA_API}/repos/${GITEA_OWNER}/${GITEA_REPO}/contents/${AI_CACHE_FILE}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `token ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
}
|
||||
);
|
||||
if (saveResp.ok) {
|
||||
const dateEl = document.getElementById('ai-cache-date');
|
||||
if (dateEl) dateEl.textContent = 'Обновлено: ' +
|
||||
new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'});
|
||||
console.info('AI cache saved to git ✓');
|
||||
} else {
|
||||
console.warn('AI cache save failed:', saveResp.status);
|
||||
}
|
||||
} catch(e) {
|
||||
console.warn('AI cache save error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════ EVENTS ═══════════════════
|
||||
document.getElementById('file-input').addEventListener('change',function(e){
|
||||
const file=e.target.files[0]; if(!file)return;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user