feat: AI Assistant at the top of the main menu; warn-only conversation budget
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 57s
Forgejo Android APK / Build signed APK (push) Successful in 2m3s

This commit is contained in:
Daniel 2026-09-09 01:25:46 +02:00
parent 7fdbcd51e6
commit c8a9f934be
4 changed files with 9 additions and 14 deletions

View file

@ -65,8 +65,7 @@
<form id="assistant-form" class="assistant-composer">
<label for="assistant-input">Clinical question</label>
<textarea id="assistant-input" rows="1" placeholder="Ask a focused clinical question..." autocomplete="off" aria-describedby="assistant-context-budget assistant-context-warning"></textarea>
<p id="assistant-context-budget" class="assistant-muted" aria-live="polite">Loading conversation limit; history and draft are counted in UTF-16 code units. The server validates each request.</p>
<textarea id="assistant-input" rows="1" placeholder="Ask a focused clinical question..." autocomplete="off" aria-describedby="assistant-context-warning"></textarea>
<div id="assistant-context-warning" role="alert" hidden></div>
<div id="assistant-attachments" class="assistant-attachments" aria-label="Attached images" hidden></div>
<div class="assistant-composer-footer">

View file

@ -211,6 +211,10 @@
</button>
</div>
<div class="sidebar-nav">
<button class="tab-btn" data-tab="assistant">
<i class="fas fa-brain" style="color:var(--purple);"></i>
<span>AI Assistant</span>
</button>
<span class="sidebar-section-label">Encounters</span>
<button class="tab-btn active" data-tab="encounter">
<i class="fas fa-comments"></i>
@ -278,10 +282,6 @@
<i class="fas fa-diagram-project"></i>
<span>Diagrams</span>
</button>
<button class="tab-btn" data-tab="assistant">
<i class="fas fa-brain" style="color:var(--purple);"></i>
<span>AI Assistant</span>
</button>
<button class="tab-btn hidden" data-tab="cms" id="cms-tab-btn">
<i class="fas fa-pen-to-square"></i>
<span>Content Manager</span>

View file

@ -363,9 +363,7 @@ test('UI sends images only with the question, clears them on success, persists t
const input = ui.document.getElementById('assistant-input');
input.value = 'x';
input.dispatchEvent(new ui.dom.window.Event('input'));
const label = ui.document.getElementById('assistant-context-budget');
assert.equal(label.textContent, '1,000 / 1,000 conversation characters (UTF-16 code units).', 'budget label stays text-only');
assert.doesNotMatch(label.textContent, /image/i);
assert.equal(ui.document.getElementById('assistant-context-budget'), null, 'the constant character counter is gone; only the approaching-limit warning remains');
// Pending attachments stay input-only until the question is sent.
ui.context.performAutosave();

View file

@ -204,8 +204,8 @@ test('native conversation UI counts UTF-16 history + draft, warns at exactly 90%
});
await loadChat(ui);
const warning = ui.document.getElementById('assistant-context-warning');
const usage = ui.document.getElementById('assistant-context-budget');
enter(ui, ''); assert.match(usage.textContent, /899 \/ 1,000.*UTF-16 code units/); assert.equal(warning.hidden, true);
assert.equal(ui.document.getElementById('assistant-context-budget'), null, 'no constant counter — warnings only');
enter(ui, ''); assert.equal(warning.hidden, true);
enter(ui, 'x'); assert.equal(warning.hidden, false); assert.match(warning.textContent, /90%/);
enter(ui, 'x'.repeat(101)); assert.match(warning.textContent, /At the conversation limit/);
const input = enter(ui, 'x'.repeat(102));
@ -266,12 +266,10 @@ test('missing/invalid metadata never fabricates a cap; authoritative refusal kee
if (url.endsWith('/chat/stream')) return json({ error: 'Environment budget refused', budget: { limit: 1000 }, code: 'CONVERSATION_LIMIT' }, 413);
});
enter(ui, '😀'.repeat(501));
assert.match(ui.document.getElementById('assistant-context-budget').textContent, /1,002.*Limit unavailable/);
assert.doesNotMatch(ui.document.getElementById('assistant-context-budget').textContent, /120,000/);
assert.equal(ui.document.getElementById('assistant-context-budget'), null, 'no fabricated counter when the limit is unavailable');
await ask(ui);
assert.equal(ui.document.getElementById('assistant-input').value, '😀'.repeat(501));
assert.equal(ui.document.querySelectorAll('.assistant-msg').length, 0);
assert.match(ui.document.getElementById('assistant-context-budget').textContent, /1,002 \/ 1,000/);
assert.match(ui.document.getElementById('assistant-context-warning').textContent, /Sending is blocked/);
await ask(ui);
assert.equal(ui.calls.filter(c => c.url.endsWith('/chat/stream')).length, 1, 'subsequent refusal is local');