feat: complete chat archive sync and mobile actions
This commit is contained in:
1 parent
bfbaef8c68
commit
fdf905eb7e
4 files changed
+90
-18
No files matched your search
+21
-11
@@ -2,8 +2,8 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
const $ = id => document.getElementById(id), api = (path, method = 'GET', body, signal) => HopeAuth.request(path, {method, ...(body === undefined ? {} : {body: JSON.stringify(body)}), signal});
|
||||
const state = {conversation: null, messages: [], run: null, workspaces: [], preferences: null, epoch: 0, stream: null, cursor: null, messageCursor: null, workspace: null, busy: false, voiceActive: false, pending: null};
|
||||
let searchTimer, listVersion = 0, action;
|
||||
const state = {conversation: null, messages: [], run: null, workspaces: [], preferences: null, epoch: 0, stream: null, cursor: null, messageCursor: null, workspace: null, busy: false, voiceActive: false, archived: false, syncCursor: 0, pending: null};
|
||||
let searchTimer, syncTimer, listVersion = 0, action, syncBusy = false;
|
||||
const enc = encodeURIComponent, terminal = run => !run || ['completed', 'failed', 'cancelled'].includes(run.status);
|
||||
const notice = text => { $('chatNotice').textContent = text; };
|
||||
const button = (text, fn, cls = 'btn btn-ghost') => { const b = document.createElement('button'); b.type = 'button'; b.className = cls; b.textContent = text; b.addEventListener('click', () => Promise.resolve().then(fn).catch(error => notice(error.message))); return b; };
|
||||
@@ -12,7 +12,9 @@
|
||||
$('primaryInput').disabled = !ready || state.busy || running;
|
||||
$('primarySend').disabled = !ready || state.busy || running || window.HopeFeatures?.hasPendingUploads() || !$('primaryInput').value.trim();
|
||||
$('cancelRun').hidden = terminal(state.run);
|
||||
for (const id of ['pinConversation', 'renameConversation', 'archiveConversation', 'deleteConversation']) $(id).disabled = !state.conversation || state.busy;
|
||||
for (const id of ['pinConversation', 'renameConversation', 'moveConversation', 'archiveConversation', 'deleteConversation']) $(id).disabled = !state.conversation || state.busy;
|
||||
$('conversationActions').hidden = !state.conversation; $('conversationMenu').hidden = !state.conversation;
|
||||
$('archiveConversation').textContent = state.conversation?.archived ? 'Unarchive' : 'Archive';
|
||||
$('pinConversation').textContent = state.conversation?.pinned ? 'Unpin' : 'Pin';
|
||||
$('chatTitle').textContent = state.conversation?.title || 'New chat';
|
||||
}
|
||||
@@ -34,7 +36,8 @@
|
||||
$('drawerOpen').onclick = () => drawer(true); $('drawerClose').onclick = $('drawerBackdrop').onclick = () => drawer(false);
|
||||
document.addEventListener('keydown', event => { if (event.key === 'Escape' && $('chatSidebar').classList.contains('drawer-open')) drawer(false); if ((event.metaKey || event.ctrlKey) && event.key === 'k') {event.preventDefault(); drawer(true); $('conversationSearch').focus();} });
|
||||
const mobile = matchMedia('(max-width: 760px)');
|
||||
function adapt() { $('chatSidebar').inert = mobile.matches && !$('chatSidebar').classList.contains('drawer-open'); if (!mobile.matches) { $('primaryChat').inert = false; $('drawerBackdrop').hidden = true; } }
|
||||
function adapt() { if (mobile.matches) $('conversationActions').setAttribute('popover', 'auto'); else {$('conversationActions').removeAttribute('popover');}
|
||||
$('chatSidebar').inert = mobile.matches && !$('chatSidebar').classList.contains('drawer-open'); if (!mobile.matches) { $('primaryChat').inert = false; $('drawerBackdrop').hidden = true; } }
|
||||
mobile.addEventListener('change', adapt); adapt();
|
||||
$('openVoice').onclick = () => { $('primaryChat').hidden = true; $('legacyVoice').hidden = false; if (mobile.matches) drawer(false); $('backToChat').focus(); };
|
||||
$('backToChat').onclick = () => { $('primaryChat').hidden = false; $('legacyVoice').hidden = true; $('primaryInput').focus(); };
|
||||
@@ -54,9 +57,11 @@
|
||||
}
|
||||
function markdown(text) {
|
||||
const root = document.createElement('div'); root.className = 'markdown'; let code = null, list = null;
|
||||
for (const line of String(text || '').split('\n')) {
|
||||
const lines = String(text || '').split('\n');
|
||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { const line = lines[lineIndex];
|
||||
if (line.startsWith('```')) { if (code) {code = null;} else {const pre = document.createElement('pre'); code = document.createElement('code'); pre.append(code); root.append(pre);} list = null; continue; }
|
||||
if (code) {code.append(document.createTextNode(line + '\n')); continue;}
|
||||
if (line.includes('|') && /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(lines[lineIndex + 1] || '')) {const table = document.createElement('table'), head = document.createElement('thead'), body = document.createElement('tbody'), wrap = document.createElement('div'); wrap.className = 'markdown-table'; const cells = value => value.trim().replace(/^\||\|$/g, '').split('|'); const addRow = (value, parent, header) => {const row = document.createElement('tr'); for (const cell of cells(value)) {const node = document.createElement(header ? 'th' : 'td'); if (header) node.scope = 'col'; inline(node, cell.trim()); row.append(node);} parent.append(row);}; addRow(line, head, true); lineIndex += 1; while (lines[lineIndex + 1]?.includes('|') && lines[lineIndex + 1].trim()) {lineIndex++; addRow(lines[lineIndex], body, false);} table.append(head, body); wrap.append(table); root.append(wrap); list = null; continue;}
|
||||
const item = /^(?:[-*] |\d+\. )(.*)$/.exec(line);
|
||||
if (item) {const type = /^\d/.test(line) ? 'ol' : 'ul'; if (!list || list.tagName.toLowerCase() !== type) {list = document.createElement(type); root.append(list);} const li = document.createElement('li'); inline(li, item[1]); list.append(li); continue;}
|
||||
list = null; if (!line.trim()) continue;
|
||||
@@ -85,7 +90,7 @@
|
||||
}
|
||||
async function list(append = false) {
|
||||
const version = ++listVersion, epoch = state.epoch;
|
||||
const params = new URLSearchParams({q: $('conversationSearch').value, archived: 'false', limit: '50'});
|
||||
const params = new URLSearchParams({q: $('conversationSearch').value, archived: String(state.archived), limit: '50'});
|
||||
if (state.workspace) params.set('workspace_id', state.workspace); if (append && state.cursor) params.set('cursor', state.cursor);
|
||||
const data = await api('/api/conversations?' + params); if (version !== listVersion || epoch !== state.epoch) return;
|
||||
if (!append) { $('pinnedConversations').replaceChildren(); $('recentConversations').replaceChildren(); }
|
||||
@@ -101,16 +106,18 @@
|
||||
function reset() { stopStream(); ++state.epoch; state.messages = []; state.messageCursor = null; state.run = null; state.pending = null; state.busy = false; selected(null); render(); notice(''); $('primaryInput').value = ''; controls(); }
|
||||
$('newConversation').onclick = () => {reset(); if (mobile.matches) drawer(false); $('primaryInput').focus();};
|
||||
$('conversationSearch').oninput = () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => list().catch(e => notice(e.message)), 200); };
|
||||
$('showArchived').onclick = () => {state.archived = !state.archived; $('showArchived').setAttribute('aria-pressed', String(state.archived)); $('recentHeading').textContent = state.archived ? 'Archived' : 'Recent'; list().catch(e => notice(e.message));};
|
||||
$('loadMoreConversations').onclick = () => list(true).catch(e => notice(e.message));
|
||||
async function update(patch) {const epoch = state.epoch; const data = await api('/api/conversations/' + enc(state.conversation.id), 'PATCH', {revision: state.conversation.revision, ...patch}); if (epoch !== state.epoch) return; selected(data.conversation); await list();}
|
||||
async function update(patch) {const epoch = state.epoch; let data; try {data = await api('/api/conversations/' + enc(state.conversation.id), 'PATCH', {revision: state.conversation.revision, ...patch});} catch (error) {await refreshSelected(false); await list(); throw new Error(error.message + ' Conversation refreshed. Review the latest version and try again.');} if (epoch !== state.epoch) return; selected(data.conversation); await list();}
|
||||
$('pinConversation').onclick = () => update({pinned: !state.conversation.pinned}).catch(e => notice(e.message));
|
||||
function dialog(title, value, fn, description = '', needsValue = true) {
|
||||
action = fn; $('chatActionTitle').textContent = title; $('chatActionDescription').textContent = description; $('chatActionValue').value = value; $('chatActionValue').hidden = !needsValue; $('chatActionLabel').hidden = !needsValue; $('chatActionValue').required = needsValue; $('chatActionError').textContent = ''; $('chatActionDialog').showModal();
|
||||
$('chatWorkspaceSelect').hidden = true; $('chatWorkspaceLabel').hidden = true; if ($('conversationActions').matches(':popover-open')) $('conversationActions').hidePopover(); action = fn; $('chatActionTitle').textContent = title; $('chatActionDescription').textContent = description; $('chatActionValue').value = value; $('chatActionValue').hidden = !needsValue; $('chatActionLabel').hidden = !needsValue; $('chatActionValue').required = needsValue; $('chatActionError').textContent = ''; $('chatActionDialog').showModal();
|
||||
}
|
||||
$('chatActionCancel').onclick = () => $('chatActionDialog').close();
|
||||
$('chatActionForm').onsubmit = async event => {event.preventDefault(); $('chatActionSave').disabled = true; try {await action($('chatActionValue').value.trim()); $('chatActionDialog').close();} catch (e) {$('chatActionError').textContent = e.message;} finally {$('chatActionSave').disabled = false;} };
|
||||
$('renameConversation').onclick = () => dialog('Rename conversation', state.conversation.title, title => update({title}));
|
||||
$('archiveConversation').onclick = () => dialog('Archive conversation?', '', async () => {await update({archived: true}); reset(); await list();}, 'The conversation is retained, but leaves Recent.', false);
|
||||
$('archiveConversation').onclick = () => dialog(state.conversation.archived ? 'Unarchive conversation?' : 'Archive conversation?', '', async () => {await update({archived: !state.conversation.archived}); reset(); await list();}, 'The conversation is retained, but leaves Recent.', false);
|
||||
$('moveConversation').onclick = () => {dialog('Move conversation', '', () => update({workspace_id: $('chatWorkspaceSelect').value || null}), 'Keep the same history in a different workspace.', false); const select = $('chatWorkspaceSelect'); select.replaceChildren(); for (const workspace of [{id:'',name:'No workspace'}, ...state.workspaces]) {const option = document.createElement('option'); option.value = workspace.id; option.textContent = workspace.name; select.append(option);} select.value = state.conversation.workspace_id || ''; select.hidden = false; $('chatWorkspaceLabel').hidden = false;};
|
||||
$('deleteConversation').onclick = () => dialog('Delete conversation?', '', async () => {await api('/api/conversations/' + enc(state.conversation.id), 'DELETE'); reset(); await list();}, 'This deletes the conversation, not your account memory.', false);
|
||||
async function branch(message, content, send = false) {const data = await api('/api/conversations/' + enc(state.conversation.id) + '/branch', 'POST', {message_id: message.id, ...(content === undefined ? {} : {content})}); await open(data.conversation.id); if (send) {$('primaryInput').value = content; await submit();} }
|
||||
function edit(message) {dialog('Edit in a new branch', message.content, content => branch(message, content, true), 'Your original conversation stays unchanged.');}
|
||||
@@ -169,10 +176,13 @@
|
||||
$('openPreferences').onclick = async () => {try {const data = await api('/api/preferences'); state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = ''; if (!HopeAuth.user) return; $('preferencesDialog').showModal();} catch(e) {notice(e.message);} };
|
||||
$('closePreferences').onclick = () => $('preferencesDialog').close();
|
||||
$('preferencesForm').onsubmit = async event => {event.preventDefault(); try {const data = await api('/api/preferences', 'PUT', {revision: state.preferences.revision, palette: $('paletteSelect').value, appearance: $('appearanceSelect').value}); state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = 'Appearance saved.';} catch(e) {$('preferencesMessage').textContent = e.message;} };
|
||||
async function refreshSelected(messages = true) {if (!state.conversation) return; const epoch = state.epoch, cid = state.conversation.id; const data = await api('/api/conversations/' + enc(cid)); if (epoch !== state.epoch || cid !== state.conversation?.id) return; selected(data.conversation); if (messages && terminal(state.run)) {const timeline = $('chatTimeline'), scroll = timeline.scrollTop; const incoming = new Map((data.messages || []).map(m => [m.id,m])); state.messages = state.messages.map(m => incoming.get(m.id) || m); for (const m of incoming.values()) if (!state.messages.some(old => old.id === m.id)) state.messages.push(m); render(); timeline.scrollTop = scroll;} }
|
||||
async function sync() {if (syncBusy || !HopeAuth.user || document.hidden) return; syncBusy = true; const epoch = state.epoch; try {const data = await api('/api/sync?after=' + state.syncCursor); if (epoch !== state.epoch) return; state.syncCursor = data.cursor; if (!data.changes?.length) return; for (const change of data.changes) {if (change.kind === 'preferences' && change.data && !$('preferencesDialog').open) {state.preferences = change.data; theme(change.data);} if (change.kind === 'conversation' && change.id === state.conversation?.id && change.deleted) {reset(); notice('This conversation was deleted on another device.');}} await Promise.all([list(), workspaces(), refreshSelected()]);} catch (error) {/* A subsequent foreground poll retries; never discard the draft. */} finally {syncBusy = false;}}
|
||||
document.addEventListener('visibilitychange', () => {if (!document.hidden) sync();});
|
||||
HopeAuth.onChange((user, changed) => {
|
||||
if (changed || !user) {reset(); state.workspace = null; state.workspaces = []; state.preferences = null; $('pinnedConversations').replaceChildren(); $('recentConversations').replaceChildren(); $('workspaceList').replaceChildren(); $('legacyVoice').hidden = true; $('primaryChat').hidden = false; $('preferencesDialog').close(); $('chatActionDialog').close(); theme({palette: 'opal', appearance: 'system'});}
|
||||
if (changed || !user) {clearInterval(syncTimer); state.syncCursor = 0; state.archived = false; reset(); state.workspace = null; state.workspaces = []; state.preferences = null; $('pinnedConversations').replaceChildren(); $('recentConversations').replaceChildren(); $('workspaceList').replaceChildren(); $('legacyVoice').hidden = true; $('primaryChat').hidden = false; $('preferencesDialog').close(); $('chatActionDialog').close(); theme({palette: 'opal', appearance: 'system'});}
|
||||
controls(); if (!user || !changed) return;
|
||||
const epoch = state.epoch;
|
||||
const epoch = state.epoch; syncTimer = setInterval(sync, 5000);
|
||||
Promise.all([list(), workspaces(), api('/api/preferences').then(data => {if (epoch === state.epoch) {state.preferences = data.preferences; theme(data.preferences);}})]).catch(e => {if (epoch === state.epoch) notice(e.message);});
|
||||
});
|
||||
})();
|
||||
+3
-3
@@ -28,8 +28,8 @@
|
||||
<button id="retryLogout" class="btn btn-secondary" hidden>Retry sign out</button>
|
||||
</section>
|
||||
<main class="app-shell" id="authenticatedApp" hidden>
|
||||
<aside id="chatSidebar" class="chat-sidebar" aria-label="Conversations"><div class="sidebar-brand"><span class="hope-mark" aria-hidden="true">✳</span><strong>Hope</strong><button id="drawerClose" class="btn btn-ghost" aria-label="Close navigation">×</button></div><button id="newConversation" class="btn new-chat">+ New chat</button><label class="sr-only" for="conversationSearch">Search conversations</label><input id="conversationSearch" type="search" placeholder="Search conversations"><nav aria-label="Chat history"><h2>Pinned</h2><div id="pinnedConversations"></div><div class="nav-heading"><h2>Workspaces</h2><button id="newWorkspace" class="btn btn-ghost" aria-label="Create workspace">+</button></div><div id="workspaceList"></div><h2>Recent</h2><div id="recentConversations"></div><button id="loadMoreConversations" class="btn btn-ghost" hidden>Load more</button></nav><div class="sidebar-footer"><button id="openVoice" class="btn btn-ghost">Voice & account</button><button id="openPreferences" class="btn btn-ghost">Appearance</button></div></aside><button id="drawerBackdrop" class="drawer-backdrop" aria-label="Close navigation" hidden></button>
|
||||
<section id="primaryChat" class="primary-chat" aria-labelledby="chatTitle"><header class="chat-topbar"><button id="drawerOpen" class="btn btn-ghost" aria-label="Open navigation" aria-expanded="false" aria-controls="chatSidebar">☰</button><h1 id="chatTitle">New chat</h1><div id="featureActions" class="feature-actions"></div><div class="conversation-actions"><button id="pinConversation" class="btn btn-ghost" disabled>Pin</button><button id="renameConversation" class="btn btn-ghost" disabled>Rename</button><button id="archiveConversation" class="btn btn-ghost" disabled>Archive</button><button id="deleteConversation" class="btn btn-ghost" disabled>Delete</button></div></header><div id="chatNotice" class="chat-notice" role="status" aria-live="polite"></div><div id="chatTimeline" class="chat-timeline" tabindex="-1" aria-label="Messages"><div class="chat-welcome"><span class="hope-mark" aria-hidden="true">✳</span><h2>A little space to think.</h2><p>Ask a question, work through an idea, or start something new.</p></div></div><div id="runProgress" class="run-progress" role="status" aria-live="polite"></div><form id="primaryComposer" class="primary-composer" aria-label="Message Hope"><label class="sr-only" for="primaryInput">Message Hope</label><textarea id="primaryInput" rows="2" placeholder="What's on your mind?" required disabled></textarea><div id="attachmentPreviews"></div><div class="composer-footer"><span>Enter to send · Shift + Enter for a new line</span><button id="cancelRun" class="btn" type="button" hidden>Stop response</button><button id="primarySend" class="btn btn-primary" disabled>Send ↑</button></div></form><p class="chat-footnote">Your chats are private to your account. Hope can make mistakes.</p></section><section id="legacyVoice" class="legacy-voice" hidden aria-label="Voice and account settings"><button id="backToChat" class="btn btn-secondary">← Back to chat</button><p>Live voice controls and account settings.</p> <header class="topbar">
|
||||
<aside id="chatSidebar" class="chat-sidebar" aria-label="Conversations"><div class="sidebar-brand"><span class="hope-mark" aria-hidden="true">✳</span><strong>Hope</strong><button id="drawerClose" class="btn btn-ghost" aria-label="Close navigation">×</button></div><button id="newConversation" class="btn new-chat">+ New chat</button><label class="sr-only" for="conversationSearch">Search conversations</label><input id="conversationSearch" type="search" placeholder="Search conversations"><nav aria-label="Chat history"><h2>Pinned</h2><div id="pinnedConversations"></div><div class="nav-heading"><h2>Workspaces</h2><button id="newWorkspace" class="btn btn-ghost" aria-label="Create workspace">+</button></div><div id="workspaceList"></div><div class="nav-heading"><h2 id="recentHeading">Recent</h2><button id="showArchived" class="btn btn-ghost" aria-pressed="false">Archived</button></div><div id="recentConversations"></div><button id="loadMoreConversations" class="btn btn-ghost" hidden>Load more</button></nav><div class="sidebar-footer"><button id="openVoice" class="btn btn-ghost">Voice & account</button><button id="openPreferences" class="btn btn-ghost">Appearance</button></div></aside><button id="drawerBackdrop" class="drawer-backdrop" aria-label="Close navigation" hidden></button>
|
||||
<section id="primaryChat" class="primary-chat" aria-labelledby="chatTitle"><header class="chat-topbar"><button id="drawerOpen" class="btn btn-ghost" aria-label="Open navigation" aria-expanded="false" aria-controls="chatSidebar">☰</button><h1 id="chatTitle">New chat</h1><div id="featureActions" class="feature-actions"></div><button id="conversationMenu" class="btn btn-ghost" popovertarget="conversationActions" aria-label="Conversation actions" hidden>•••</button><div id="conversationActions" class="conversation-actions"><button id="pinConversation" class="btn btn-ghost" disabled>Pin</button><button id="renameConversation" class="btn btn-ghost" disabled>Rename</button><button id="moveConversation" class="btn btn-ghost" disabled>Move</button><button id="archiveConversation" class="btn btn-ghost" disabled>Archive</button><button id="deleteConversation" class="btn btn-ghost" disabled>Delete</button></div></header><div id="chatNotice" class="chat-notice" role="status" aria-live="polite"></div><div id="chatTimeline" class="chat-timeline" tabindex="-1" aria-label="Messages"><div class="chat-welcome"><span class="hope-mark" aria-hidden="true">✳</span><h2>A little space to think.</h2><p>Ask a question, work through an idea, or start something new.</p></div></div><div id="runProgress" class="run-progress" role="status" aria-live="polite"></div><form id="primaryComposer" class="primary-composer" aria-label="Message Hope"><label class="sr-only" for="primaryInput">Message Hope</label><textarea id="primaryInput" rows="2" placeholder="What's on your mind?" required disabled></textarea><div id="attachmentPreviews"></div><div class="composer-footer"><span>Enter to send · Shift + Enter for a new line</span><button id="cancelRun" class="btn" type="button" hidden>Stop response</button><button id="primarySend" class="btn btn-primary" disabled>Send ↑</button></div></form><p class="chat-footnote">Your chats are private to your account. Hope can make mistakes.</p></section><section id="legacyVoice" class="legacy-voice" hidden aria-label="Voice and account settings"><button id="backToChat" class="btn btn-secondary">← Back to chat</button><p>Live voice controls and account settings.</p> <header class="topbar">
|
||||
<div class="brand">
|
||||
<span class="brand-glyph" aria-hidden="true"><i></i></span>
|
||||
<div>
|
||||
@@ -207,7 +207,7 @@
|
||||
<p id="accountDialogMessage" role="status"></p><div class="dialog-actions"><button id="cancelAccountAction" type="button" class="btn btn-secondary">Cancel</button><button id="confirmAccountAction" class="btn btn-danger">Confirm</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="chatActionDialog" aria-labelledby="chatActionTitle"><form id="chatActionForm" class="account-form"><h2 id="chatActionTitle"></h2><p id="chatActionDescription"></p><label id="chatActionLabel" for="chatActionValue">Name</label><textarea id="chatActionValue" rows="3" required></textarea><p id="chatActionError" role="status"></p><div class="dialog-actions"><button id="chatActionCancel" type="button" class="btn">Cancel</button><button id="chatActionSave" class="btn btn-primary">Save</button></div></form></dialog><dialog id="preferencesDialog" aria-labelledby="preferencesTitle"><h2 id="preferencesTitle">Appearance</h2><form id="preferencesForm" class="account-form"><label for="paletteSelect">Palette</label><select id="paletteSelect"><option value="opal">Opal</option><option value="graphite">Graphite</option><option value="ocean">Ocean</option><option value="sage">Sage</option><option value="lavender">Lavender</option></select><label for="appearanceSelect">Appearance</label><select id="appearanceSelect"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select><p id="preferencesMessage" role="status"></p><div class="dialog-actions"><button id="closePreferences" class="btn" type="button">Close</button><button class="btn btn-primary">Save appearance</button></div></form></dialog><script src="auth.js"></script>
|
||||
<dialog id="chatActionDialog" aria-labelledby="chatActionTitle"><form id="chatActionForm" class="account-form"><h2 id="chatActionTitle"></h2><p id="chatActionDescription"></p><label id="chatActionLabel" for="chatActionValue">Name</label><textarea id="chatActionValue" rows="3" required></textarea><label id="chatWorkspaceLabel" for="chatWorkspaceSelect" hidden>Workspace</label><select id="chatWorkspaceSelect" hidden></select><p id="chatActionError" role="status"></p><div class="dialog-actions"><button id="chatActionCancel" type="button" class="btn">Cancel</button><button id="chatActionSave" class="btn btn-primary">Save</button></div></form></dialog><dialog id="preferencesDialog" aria-labelledby="preferencesTitle"><h2 id="preferencesTitle">Appearance</h2><form id="preferencesForm" class="account-form"><label for="paletteSelect">Palette</label><select id="paletteSelect"><option value="opal">Opal</option><option value="graphite">Graphite</option><option value="ocean">Ocean</option><option value="sage">Sage</option><option value="lavender">Lavender</option></select><label for="appearanceSelect">Appearance</label><select id="appearanceSelect"><option value="system">System</option><option value="light">Light</option><option value="dark">Dark</option></select><p id="preferencesMessage" role="status"></p><div class="dialog-actions"><button id="closePreferences" class="btn" type="button">Close</button><button class="btn btn-primary">Save appearance</button></div></form></dialog><script src="auth.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="chat.js"></script>
|
||||
<script src="features.js"></script>
|
||||
|
||||
@@ -1523,3 +1523,23 @@ dialog h2 {font-size:22px;margin-bottom:16px;}
|
||||
|
||||
.primary-composer .btn-primary {min-width:84px;flex:0 0 auto;}
|
||||
.primary-composer .btn-primary:disabled,.account-form .btn-primary:disabled,.login-gate .btn-primary:disabled {display:inline-flex;}
|
||||
|
||||
.app-shell {padding:0;border:0;border-radius:0;}
|
||||
#conversationMenu {display:none;min-width:44px;min-height:44px;}
|
||||
#showArchived {font-size:12px;min-height:40px;padding:8px;}
|
||||
.markdown-table {max-width:100%;overflow:auto;margin:14px 0;}
|
||||
.markdown-table table {border-collapse:collapse;width:100%;font-size:14px;}
|
||||
.markdown-table th,.markdown-table td {padding:10px 12px;border:1px solid var(--line);text-align:left;}
|
||||
.markdown-table th {background:var(--surface);font-weight:600;}
|
||||
.primary-composer textarea:focus:not(:focus-visible) {outline:none;box-shadow:none;}
|
||||
@media(max-width:760px) {
|
||||
#conversationMenu:not([hidden]) {display:inline-flex;}
|
||||
.conversation-actions {width:220px;padding:8px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:var(--ink);margin:76px 12px 0 auto;}
|
||||
.conversation-actions:not(:popover-open) {display:none;}
|
||||
.conversation-actions:popover-open {display:flex;flex-direction:column;}
|
||||
.conversation-actions .btn {min-height:44px;justify-content:flex-start;text-align:left;width:100%;font-size:14px;}
|
||||
.chat-topbar {flex-wrap:nowrap;}
|
||||
.chat-topbar h1 {min-width:0;}
|
||||
.feature-actions {max-width:120px;}
|
||||
#drawerOpen {min-height:44px;min-width:44px;}
|
||||
}
|
||||
Reference in new issue
Block a user