Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-04-29 - [Search Accessibility and Experience]
**Learning:** Empty search results should provide clear feedback to the user and be announced by screen readers using ARIA live regions. Additionally, search state should be maintained across UI updates or explicitly cleared during major transitions to prevent confusing the user.
**Action:** Implement "No results found" messages with `role="status"` and `aria-live="polite"`. Ensure search inputs are cleared during state transitions like creating a new conversation.
22 changes: 11 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">AI <span>Studio</span></div>
<button class="sidebar-collapse-btn" id="sidebar-collapse-btn" title="Collapse">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 19l-7-7 7-7"/><path d="M18 19l-7-7 7-7" opacity="0.4"/></svg>
<button class="sidebar-collapse-btn" id="sidebar-collapse-btn" title="Collapse sidebar" aria-label="Collapse sidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M11 19l-7-7 7-7"/><path d="M18 19l-7-7 7-7" opacity="0.4"/></svg>
</button>
</div>
<button class="new-chat-btn" id="new-chat-btn">
Expand Down Expand Up @@ -175,8 +175,8 @@

<div id="app">
<header>
<button class="hamburger-btn" id="hamburger-btn" title="Toggle sidebar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
<button class="hamburger-btn" id="hamburger-btn" title="Toggle sidebar" aria-label="Toggle sidebar">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="logo-dot"></div>
<h1>AI <span>Studio</span></h1>
Expand Down Expand Up @@ -232,16 +232,16 @@ <h3>Start a Conversation</h3>
</div>
<div class="input-row">
<input type="file" id="file-input" multiple accept="image/*,.pdf,.txt,.md,.csv,.js,.py,.ts,.html,.css,.json" style="display:none">
<button class="attach-btn" id="attach-btn" title="Attach file or image (images, PDFs, text)">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
<button class="attach-btn" id="attach-btn" title="Attach file or image (images, PDFs, text)" aria-label="Attach file">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
</button>
<button class="mic-btn" id="voice-input-btn" title="Voice input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
<button class="mic-btn" id="voice-input-btn" title="Voice input" aria-label="Voice input">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
<textarea id="chatInput" rows="1" placeholder="Send a message…"></textarea>
<button class="send-btn" id="chatSendBtn">➀</button>
<button class="stop-btn" id="chatStopBtn" title="Stop generation">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
<button class="send-btn" id="chatSendBtn" aria-label="Send message">➀</button>
<button class="stop-btn" id="chatStopBtn" title="Stop generation" aria-label="Stop generation">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
</button>
</div>
<p class="hint">Ctrl+Enter to send Β· Shift+Enter for newline Β· ? for shortcuts</p>
Expand Down
34 changes: 31 additions & 3 deletions js/conversations.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ function newChat() {
id: crypto.randomUUID(),
title: '',
model: S.currentModel,
createdAt: Date.now(),
updatedAt: Date.now(),
createdAt: Date.now(), updatedAt: Date.now(),
messages: [],
pinned: false,
};
S.conversations[conv.id] = conv;
S.activeConvId = conv.id;
S.chatMessages = [];
saveConvs();

// Clear search input on new chat
const searchInput = document.getElementById('sidebar-search');
if (searchInput) searchInput.value = '';

renderSidebar();
if (typeof renderChatMessages === 'function') renderChatMessages();
document.getElementById('chatInput').focus();
Expand Down Expand Up @@ -118,10 +122,17 @@ async function autoTitleConv(convId, userText) {
}

// ── Sidebar rendering ──────────────────────────────────────────────────────
function renderSidebar(searchQuery = '') {
function renderSidebar(searchQuery = null) {
const container = document.getElementById('sidebar-conversations');
if (!container) return;
container.innerHTML = '';

// Default to current input value if no query provided
if (searchQuery === null) {
const input = document.getElementById('sidebar-search');
searchQuery = input ? input.value : '';
}

const q = searchQuery.toLowerCase().trim();
let arr = Object.values(S.conversations).sort((a, b) => b.updatedAt - a.updatedAt);

Expand Down Expand Up @@ -152,11 +163,28 @@ function renderSidebar(searchQuery = '') {
else groups['Older'].push(c);
}

let anyVisible = false;
if (pinned.length) {
anyVisible = true;
appendGroupLabel(container, 'πŸ“Œ Pinned');
pinned.forEach(c => container.appendChild(makeConvEl(c)));
}

for (const [label, items] of Object.entries(groups)) {
if (!items.length) continue;
anyVisible = true;
appendGroupLabel(container, label);
items.forEach(c => container.appendChild(makeConvEl(c)));
}

if (!anyVisible && q) {
const none = document.createElement('div');
none.className = 'model-no-results'; // Reuse existing styles
none.setAttribute('role', 'status');
none.setAttribute('aria-live', 'polite');
none.textContent = `No chats match "${searchQuery}"`;
container.appendChild(none);
}
}

function appendGroupLabel(container, text) {
Expand Down
2 changes: 2 additions & 0 deletions js/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ function renderModelList(filter = '') {
if (!anyVisible) {
const none = document.createElement('div');
none.className = 'model-no-results';
none.setAttribute('role', 'status');
none.setAttribute('aria-live', 'polite');
none.textContent = `No models match "${filter}"`;
list.appendChild(none);
}
Expand Down
7 changes: 6 additions & 1 deletion js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,10 @@ function readFileAsText(file) {
function copyCodeBlock(btn) {
const code = btn.closest('pre')?.querySelector('code');
if (!code) return;
navigator.clipboard.writeText(code.textContent).then(() => toast('Code copied'));
navigator.clipboard.writeText(code.textContent).then(() => {
toast('Code copied');
const oldText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = oldText; }, 2000);
});
}