feat(onboarding): improve setup UX with clickable triggers and auto-fill buttons
- Turn the "/setup" text on the welcome screen and fallback state into a clickable link that automatically runs the setup command. - Add an interactive down-arrow "Use in Chat" button next to copy button on typewriter-generated setup code blocks. - Programmatically trim the "..." placeholder when inserting API keys, focusing the cursor right after "sk-". - Implement click-delegation for supported provider spans and raw code elements inside the setup guide to instantly pre-populate the input bar.
This commit is contained in:
@@ -928,7 +928,7 @@
|
||||
<div class="chat-meta-overlay"><span id="current-meta">Odysseus Chat</span><span id="current-meta-count" class="chat-meta-count" aria-hidden="true"></span><span id="session-cost-display" class="session-cost-display" style="display:none;"></span><span class="export-dropdown-wrap" id="export-dropdown-wrap"><button type="button" class="export-dl-btn" id="export-dl-btn" title="More"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg></button><div class="export-dropdown-menu" id="export-dropdown-menu"><div class="export-dropdown-item" id="export-rename-btn"><span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></span><span>Rename</span></div><div class="export-dropdown-item" id="export-copy-btn"><span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></span><span>Copy Chat</span></div><div class="export-dropdown-item" id="export-pdf-btn"><span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><path d="M9 15v-2h2a1.5 1.5 0 0 1 0 3H9z"/></svg></span><span>PDF</span></div><div class="export-dropdown-item" id="export-doc-btn"><span class="dropdown-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg></span><span>Save to Documents</span></div></div></span></div> </div>
|
||||
<div id="welcome-screen">
|
||||
<div class="welcome-name"><svg class="welcome-boat" viewBox="0 0 32 32"><path d="M16 4L16 22L6 22Z" fill="currentColor"/><path d="M16 8L16 22L24 22Z" fill="currentColor" opacity="0.6"/><path d="M4 24Q10 20 16 24Q22 28 28 24" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round"/></svg>Odysseus</div>
|
||||
<div class="welcome-sub" id="welcome-sub">Welcome, type /setup to get started.</div>
|
||||
<div class="welcome-sub" id="welcome-sub">Welcome, <span class="setup-trigger-link" style="color:var(--accent,var(--red));font-weight:600;cursor:pointer;text-decoration:underline;" title="Click to launch setup">type /setup</span> to get started.</div>
|
||||
<div class="welcome-tip" id="welcome-tip"></div>
|
||||
<button type="button" class="incognito-btn" id="incognito-btn" title="Enable Nobody mode — no memory, no history saved">
|
||||
<svg class="eye-open" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -1266,10 +1266,10 @@
|
||||
<div class="modal-body">
|
||||
<div style="margin-bottom: 12px;">
|
||||
<label for="session-name-input" style="display: block; margin-bottom: 6px; font-weight: 500;">Session Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="session-name-input"
|
||||
placeholder="Enter session name"
|
||||
<input
|
||||
type="text"
|
||||
id="session-name-input"
|
||||
placeholder="Enter session name"
|
||||
style="width: 100%; padding: 8px; border-radius: 4px;"
|
||||
/>
|
||||
</div>
|
||||
@@ -2106,7 +2106,7 @@
|
||||
|
||||
<!-- ═══ SYSTEM TAB ═══ -->
|
||||
<div data-settings-panel="system" class="hidden">
|
||||
|
||||
|
||||
<div class="admin-card">
|
||||
<h2><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:5px;opacity:0.6"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>Data Backup</h2>
|
||||
<div class="admin-toggle-sub" style="margin-bottom:8px">Export or import your user data (memories, presets, settings, skills, preferences) as a JSON file.</div>
|
||||
|
||||
@@ -553,7 +553,7 @@ export async function refreshModels(force = false) {
|
||||
box.appendChild(noModels);
|
||||
// No endpoints yet: keep the welcome screen focused on first setup.
|
||||
const welcomeSub = document.getElementById('welcome-sub');
|
||||
if (welcomeSub) welcomeSub.innerHTML = 'Type <span style="color:var(--accent,var(--red));font-weight:600">/setup</span> to get started.';
|
||||
if (welcomeSub) welcomeSub.innerHTML = 'Type <span class="setup-trigger-link" style="color:var(--accent,var(--red));font-weight:600;cursor:pointer;text-decoration:underline;" title="Click to launch setup">/setup</span> to get started.';
|
||||
const welcomeTip = document.getElementById('welcome-tip');
|
||||
if (welcomeTip) welcomeTip.textContent = 'Type /setup, then choose Local models or API.';
|
||||
} else {
|
||||
|
||||
@@ -152,8 +152,8 @@ function _setupReply(text, remember = true) {
|
||||
|
||||
function _showSetupEndpointChoices() {
|
||||
const providers = SETUP_PROVIDER_NAMES.map(name =>
|
||||
'<span>' + name + '</span>'
|
||||
).join(', ');
|
||||
'<span class="setup-clickable-provider" style="cursor:pointer;text-decoration:underline;margin-right:8px;" title="Click to setup ' + name + '">' + name + '</span>'
|
||||
).join(' ');
|
||||
return slashReply(
|
||||
'<div class="setup-guide-no-censor" style="display:grid;gap:10px;">' +
|
||||
'<div>' +
|
||||
@@ -162,14 +162,14 @@ function _showSetupEndpointChoices() {
|
||||
'<div style="border:1px solid var(--border);border-radius:8px;padding:10px 12px;background:color-mix(in srgb,var(--bg) 88%,var(--fg) 12%);">' +
|
||||
'<div style="font-weight:700;margin-bottom:6px;">' + SETUP_LOCAL_ICON + 'Local setup</div>' +
|
||||
'<div>Paste endpoint URL in chat (example):</div>' +
|
||||
'<pre style="margin:4px 0 0;"><code>http://localhost:11434/v1</code></pre>' +
|
||||
'<pre style="margin:4px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">http://localhost:11434/v1</code></pre>' +
|
||||
'<div style="margin-top:4px;">or</div>' +
|
||||
'<pre style="margin:2px 0 0;"><code>http://llm-host.local:8000/v1</code></pre>' +
|
||||
'<pre style="margin:2px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">http://llm-host.local:8000/v1</code></pre>' +
|
||||
'</div>' +
|
||||
'<div style="border:1px solid var(--border);border-radius:8px;padding:10px 12px;background:color-mix(in srgb,var(--bg) 88%,var(--fg) 12%);">' +
|
||||
'<div style="font-weight:700;margin-bottom:6px;">' + SETUP_API_ICON + 'API setup</div>' +
|
||||
'<div>Paste provider name then API key (example):</div>' +
|
||||
'<pre style="margin:4px 0 0;"><code>deepseek sk-...</code></pre>' +
|
||||
'<pre style="margin:4px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">deepseek sk-...</code></pre>' +
|
||||
'<div style="margin-top:8px;font-size:1em;"><span>Supported providers:</span><br>' + providers + '</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
@@ -201,7 +201,9 @@ function _showSetupEndpointChoicesStreamed(options = {}) {
|
||||
text: 'deepseek sk-...',
|
||||
copyText: 'deepseek sk-...',
|
||||
},
|
||||
{ kind: 'p', html: '<strong>Supported providers:</strong><br>' + SETUP_PROVIDER_NAMES.join(', ') },
|
||||
{ kind: 'p', html: '<strong>Supported providers:</strong><br>' + SETUP_PROVIDER_NAMES.map(name =>
|
||||
'<span class="setup-clickable-provider" style="cursor:pointer;text-decoration:underline;margin-right:8px;" title="Click to setup ' + name + '">' + name + '</span>'
|
||||
).join(' ') },
|
||||
];
|
||||
return typewriterBlocksReply(blocks, { gap: '4px', bodyClass: 'setup-guide-no-censor', interval: 3 });
|
||||
}
|
||||
@@ -388,10 +390,36 @@ function typewriterBlocksReply(blocks, options = {}) {
|
||||
pre.style.margin = '0';
|
||||
const code = document.createElement('code');
|
||||
pre.appendChild(code);
|
||||
const useBtn = document.createElement('button');
|
||||
useBtn.type = 'button';
|
||||
useBtn.className = 'use-code';
|
||||
useBtn.title = 'Use in Chat';
|
||||
useBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg>';
|
||||
const copyText = block.copyText || block.text || '';
|
||||
const useNow = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
let text = copyText;
|
||||
if (text.includes('sk-...')) {
|
||||
text = text.replace('sk-...', 'sk-');
|
||||
}
|
||||
const messageInput = document.getElementById('message');
|
||||
if (messageInput) {
|
||||
messageInput.value = text;
|
||||
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
messageInput.focus();
|
||||
messageInput.setSelectionRange(text.length, text.length);
|
||||
}
|
||||
useBtn.classList.add('used');
|
||||
setTimeout(() => useBtn.classList.remove('used'), 1200);
|
||||
};
|
||||
useBtn.addEventListener('pointerdown', useNow);
|
||||
useBtn.addEventListener('click', useNow);
|
||||
pre.appendChild(useBtn);
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'copy-code';
|
||||
const copyText = block.copyText || block.text || '';
|
||||
btn.setAttribute('data-code', copyText);
|
||||
btn.title = 'Copy';
|
||||
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
||||
@@ -5907,6 +5935,60 @@ async function handleSlashCommand(input) {
|
||||
export function initSlashCommands(deps) {
|
||||
API_BASE = deps.apiBase || '';
|
||||
if (deps.isStreaming) _isStreamingFn = deps.isStreaming;
|
||||
|
||||
// Global delegation for onboarding and setup clicks
|
||||
document.addEventListener('click', (e) => {
|
||||
// 1. Check for clicking the "/setup" trigger link on the welcome screen
|
||||
const trigger = e.target.closest('.setup-trigger-link');
|
||||
if (trigger) {
|
||||
e.preventDefault();
|
||||
const messageInput = document.getElementById('message');
|
||||
if (messageInput) {
|
||||
messageInput.value = '/setup';
|
||||
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
messageInput.focus();
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
if (chatForm) {
|
||||
chatForm.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check for clicking a clickable provider inside the setup guide
|
||||
const providerEl = e.target.closest('.setup-clickable-provider');
|
||||
if (providerEl) {
|
||||
e.preventDefault();
|
||||
const providerName = providerEl.textContent.trim();
|
||||
const messageInput = document.getElementById('message');
|
||||
if (messageInput) {
|
||||
const text = providerName + ' sk-';
|
||||
messageInput.value = text;
|
||||
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
messageInput.focus();
|
||||
messageInput.setSelectionRange(text.length, text.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check for clicking a clickable code block inside the setup guide
|
||||
const codeEl = e.target.closest('.setup-clickable-code');
|
||||
if (codeEl) {
|
||||
e.preventDefault();
|
||||
let text = codeEl.textContent.trim();
|
||||
if (text.includes('sk-...')) {
|
||||
text = text.replace('sk-...', 'sk-');
|
||||
}
|
||||
const messageInput = document.getElementById('message');
|
||||
if (messageInput) {
|
||||
messageInput.value = text;
|
||||
messageInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
messageInput.focus();
|
||||
messageInput.setSelectionRange(text.length, text.length);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3367,6 +3367,33 @@ body.bg-pattern-sparkles {
|
||||
border-color: var(--accent-primary, var(--red));
|
||||
background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, var(--bg));
|
||||
}
|
||||
pre .use-code {
|
||||
position:absolute; right:42px; top:6px;
|
||||
background:var(--bg); color:var(--fg);
|
||||
border:1px solid var(--border); border-radius:6px;
|
||||
width:28px; height:28px; padding:0; cursor:pointer;
|
||||
opacity:0; transition: opacity .15s, color .15s, border-color .15s;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
}
|
||||
pre .use-code.bottom { top:auto; bottom:6px; }
|
||||
pre:hover .use-code { opacity:0.7; }
|
||||
pre .use-code:hover { opacity:1; }
|
||||
pre .use-code.used {
|
||||
opacity: 1;
|
||||
color: var(--color-save-green, #4caf50);
|
||||
border-color: var(--color-save-green, #4caf50);
|
||||
background: color-mix(in srgb, var(--color-save-green, #4caf50) 18%, var(--bg));
|
||||
animation: code-copy-pulse 0.36s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
.setup-trigger-link, .setup-clickable-provider, .setup-clickable-code {
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
.setup-trigger-link:hover,
|
||||
.setup-clickable-provider:hover,
|
||||
.setup-clickable-code:hover {
|
||||
color: var(--accent, var(--red)) !important;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Tapping the code body (not a button) toggles the overlay buttons off so
|
||||
they stop covering the text on touch screens. Tap again to bring back. */
|
||||
|
||||
Reference in New Issue
Block a user