Changeset 0.17.2 (#77)
This commit is contained in:
@@ -693,9 +693,40 @@ async function deleteAdminExtension(id, name) {
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// Track CM6 editor instances per extension edit form
|
||||
var _extEditors = {};
|
||||
|
||||
// Listen for theme changes to update CM6 editors
|
||||
if (typeof Events !== 'undefined') {
|
||||
Events.on('theme.changed', (data) => {
|
||||
const isDark = data.resolved === 'dark';
|
||||
for (const editors of Object.values(_extEditors)) {
|
||||
editors.manifest?.setDarkMode(isDark);
|
||||
editors.script?.setDarkMode(isDark);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for keymap changes to update CM6 editors in real time
|
||||
Events.on('keymap.changed', (data) => {
|
||||
const mode = data.mode || 'standard';
|
||||
for (const editors of Object.values(_extEditors)) {
|
||||
editors.manifest?.setKeymap(mode);
|
||||
editors.script?.setKeymap(mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editAdminExtension(id) {
|
||||
// Close any existing edit form
|
||||
document.querySelectorAll('.ext-edit-form').forEach(el => el.remove());
|
||||
// Close any existing edit form (and destroy CM6 instances)
|
||||
document.querySelectorAll('.ext-edit-form').forEach(el => {
|
||||
const eid = el.dataset.extId;
|
||||
if (_extEditors[eid]) {
|
||||
_extEditors[eid].manifest?.destroy();
|
||||
_extEditors[eid].script?.destroy();
|
||||
delete _extEditors[eid];
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
|
||||
const ext = (UI._adminExtensions || []).find(e => e.id === id);
|
||||
if (!ext) return UI.toast('Extension not found', 'error');
|
||||
@@ -716,6 +747,15 @@ function editAdminExtension(id) {
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// Use container divs instead of textareas when CM6 is available
|
||||
const useCM6 = !!window.CM?.codeEditor;
|
||||
const manifestEditor = useCM6
|
||||
? `<div id="extEdit-manifest-${id}" style="width:100%;min-height:180px;border:1px solid var(--border);border-radius:var(--radius,4px);overflow:hidden"></div>`
|
||||
: `<textarea id="extEdit-manifest-${id}" rows="8" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2">${esc(manifestJSON)}</textarea>`;
|
||||
const scriptEditor = useCM6
|
||||
? `<div id="extEdit-script-${id}" style="width:100%;min-height:320px;border:1px solid var(--border);border-radius:var(--radius,4px);overflow:hidden"></div>`
|
||||
: `<textarea id="extEdit-script-${id}" rows="16" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2;white-space:pre">${esc(script)}</textarea>`;
|
||||
|
||||
const formHTML = `
|
||||
<div class="ext-edit-form" data-ext-id="${id}" style="border-top:1px solid var(--border);padding:12px 0;margin-top:8px">
|
||||
${systemWarning}
|
||||
@@ -729,14 +769,14 @@ function editAdminExtension(id) {
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:8px">
|
||||
<label style="font-size:12px;font-weight:600">Manifest JSON <span class="text-muted">(without _script)</span></label>
|
||||
<textarea id="extEdit-manifest-${id}" rows="8" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2">${esc(manifestJSON)}</textarea>
|
||||
${manifestEditor}
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom:8px">
|
||||
<label style="font-size:12px;font-weight:600">Script <span class="text-muted">(${script.length.toLocaleString()} chars)</span></label>
|
||||
<textarea id="extEdit-script-${id}" rows="16" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2;white-space:pre">${esc(script)}</textarea>
|
||||
${scriptEditor}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button class="btn-small" onclick="this.closest('.ext-edit-form').remove()">Cancel</button>
|
||||
<button class="btn-small" onclick="_closeExtEditForm('${id}')">Cancel</button>
|
||||
<button class="btn-small btn-primary" onclick="saveAdminExtension('${id}')">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -748,42 +788,80 @@ function editAdminExtension(id) {
|
||||
if (row.querySelector(`[onclick*="editAdminExtension('${id}')"]`)) {
|
||||
row.insertAdjacentHTML('afterend', formHTML);
|
||||
|
||||
// Tab key inserts tab in script textarea
|
||||
const scriptEl = document.getElementById(`extEdit-script-${id}`);
|
||||
if (scriptEl) {
|
||||
scriptEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = scriptEl.selectionStart;
|
||||
const end = scriptEl.selectionEnd;
|
||||
scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end);
|
||||
scriptEl.selectionStart = scriptEl.selectionEnd = start + 4;
|
||||
}
|
||||
});
|
||||
if (useCM6) {
|
||||
// Get user keybinding preference
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
const keymapMode = prefs.editorKeymap || 'standard';
|
||||
|
||||
// Initialize CM6 editors
|
||||
_extEditors[id] = {
|
||||
manifest: CM.codeEditor(document.getElementById(`extEdit-manifest-${id}`), {
|
||||
language: 'json',
|
||||
value: manifestJSON,
|
||||
lineNumbers: true,
|
||||
keymap: keymapMode,
|
||||
}),
|
||||
script: CM.codeEditor(document.getElementById(`extEdit-script-${id}`), {
|
||||
language: 'javascript',
|
||||
value: script,
|
||||
lineNumbers: true,
|
||||
keymap: keymapMode,
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
// Fallback: Tab key inserts tab in script textarea
|
||||
const scriptEl = document.getElementById(`extEdit-script-${id}`);
|
||||
if (scriptEl) {
|
||||
scriptEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = scriptEl.selectionStart;
|
||||
const end = scriptEl.selectionEnd;
|
||||
scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end);
|
||||
scriptEl.selectionStart = scriptEl.selectionEnd = start + 4;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _closeExtEditForm(id) {
|
||||
if (_extEditors[id]) {
|
||||
_extEditors[id].manifest?.destroy();
|
||||
_extEditors[id].script?.destroy();
|
||||
delete _extEditors[id];
|
||||
}
|
||||
document.querySelector(`.ext-edit-form[data-ext-id="${id}"]`)?.remove();
|
||||
}
|
||||
|
||||
async function saveAdminExtension(id) {
|
||||
const nameEl = document.getElementById(`extEdit-name-${id}`);
|
||||
const descEl = document.getElementById(`extEdit-desc-${id}`);
|
||||
const manifestEl = document.getElementById(`extEdit-manifest-${id}`);
|
||||
const scriptEl = document.getElementById(`extEdit-script-${id}`);
|
||||
if (!nameEl || !manifestEl || !scriptEl) return;
|
||||
|
||||
// Read from CM6 if available, otherwise from textarea
|
||||
const editors = _extEditors[id];
|
||||
const manifestText = editors?.manifest
|
||||
? editors.manifest.getValue()
|
||||
: document.getElementById(`extEdit-manifest-${id}`)?.value;
|
||||
const scriptText = editors?.script
|
||||
? editors.script.getValue()
|
||||
: document.getElementById(`extEdit-script-${id}`)?.value;
|
||||
|
||||
if (!nameEl || manifestText == null || scriptText == null) return;
|
||||
|
||||
// Parse manifest and merge _script back in
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(manifestEl.value.trim() || '{}');
|
||||
manifest = JSON.parse((manifestText || '').trim() || '{}');
|
||||
} catch (e) {
|
||||
return UI.toast('Invalid manifest JSON: ' + e.message, 'error');
|
||||
}
|
||||
|
||||
const script = scriptEl.value;
|
||||
if (script.trim()) {
|
||||
manifest._script = script;
|
||||
if (scriptText.trim()) {
|
||||
manifest._script = scriptText;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -793,6 +871,7 @@ async function saveAdminExtension(id) {
|
||||
manifest: manifest,
|
||||
});
|
||||
UI.toast('Extension updated — reload page to apply changes');
|
||||
_closeExtEditForm(id);
|
||||
await UI.loadAdminExtensions();
|
||||
} catch (e) {
|
||||
UI.toast(e.message, 'error');
|
||||
|
||||
@@ -721,7 +721,6 @@ function initAttachments() {
|
||||
function _initAttachmentListeners() {
|
||||
const attachBtn = document.getElementById('attachBtn');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const chatArea = document.getElementById('chatMessages');
|
||||
const lightbox = document.getElementById('lightbox');
|
||||
|
||||
@@ -741,8 +740,12 @@ function _initAttachmentListeners() {
|
||||
}
|
||||
|
||||
// ── Smart Paste ─────────────────────────
|
||||
if (messageInput) {
|
||||
messageInput.addEventListener('paste', (e) => {
|
||||
// Attach to the active input element (CM6 contentDOM or textarea)
|
||||
const pasteTarget = (typeof ChatInput !== 'undefined' && ChatInput.getDom())
|
||||
? ChatInput.getDom()
|
||||
: document.getElementById('messageInput');
|
||||
if (pasteTarget) {
|
||||
pasteTarget.addEventListener('paste', (e) => {
|
||||
if (!App.storageConfigured) return; // fall through to normal paste
|
||||
|
||||
const items = [...(e.clipboardData?.items || [])];
|
||||
|
||||
@@ -4,6 +4,75 @@
|
||||
// Chat management, send, stream, regenerate, edit, branch,
|
||||
// summarize, per-chat model persistence.
|
||||
|
||||
// ── Chat Input Abstraction ──────────────────
|
||||
// Wraps CM6 editor (when available) or fallback textarea.
|
||||
// All code that reads/writes the message input goes through this.
|
||||
|
||||
const ChatInput = {
|
||||
_editor: null, // CM6 instance, set during init
|
||||
_textarea: null, // fallback textarea element
|
||||
|
||||
getValue() {
|
||||
if (this._editor) return this._editor.getValue();
|
||||
return this._textarea?.value || '';
|
||||
},
|
||||
|
||||
setValue(text) {
|
||||
if (this._editor) {
|
||||
this._editor.setValue(text || '');
|
||||
} else if (this._textarea) {
|
||||
this._textarea.value = text || '';
|
||||
this._textarea.style.height = 'auto';
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
if (this._editor) this._editor.focus();
|
||||
else this._textarea?.focus();
|
||||
},
|
||||
|
||||
/** Get the DOM element (for paste listeners, etc.) */
|
||||
getDom() {
|
||||
if (this._editor) return this._editor.getView().contentDOM;
|
||||
return this._textarea;
|
||||
},
|
||||
|
||||
/** Get the wrapper DOM element (for resize, etc.) */
|
||||
getWrapDom() {
|
||||
if (this._editor) return this._editor.getView().dom;
|
||||
return this._textarea;
|
||||
},
|
||||
|
||||
/** Initialize — try CM6, fall back to textarea */
|
||||
init() {
|
||||
this._textarea = document.getElementById('messageInput');
|
||||
const wrap = document.getElementById('messageInputWrap');
|
||||
|
||||
if (window.CM?.chatInput && wrap) {
|
||||
// Hide the textarea, create CM6 editor in its place
|
||||
this._textarea.style.display = 'none';
|
||||
this._editor = CM.chatInput(wrap, {
|
||||
placeholder: 'Send a message...',
|
||||
onSubmit: () => sendMessage(),
|
||||
onChange: () => updateInputTokens(),
|
||||
maxHeight: 200,
|
||||
});
|
||||
DebugLog?.push?.('chat', '[CM6] Chat input initialized');
|
||||
} else {
|
||||
// Fallback: textarea with manual event handling
|
||||
DebugLog?.push?.('chat', '[CM6] Not available — using textarea fallback');
|
||||
this._textarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||
});
|
||||
this._textarea.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
|
||||
updateInputTokens();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// ── Summarize & Continue ──────────────────
|
||||
|
||||
async function summarizeAndContinue() {
|
||||
@@ -266,7 +335,7 @@ async function newChat() {
|
||||
Tokens._warningDismissed = false;
|
||||
updateContextWarning(); updateChatTokenCount();
|
||||
updateInputTokens();
|
||||
document.getElementById('messageInput').focus();
|
||||
ChatInput.focus();
|
||||
if (window.innerWidth <= 768) {
|
||||
document.getElementById('sidebar').classList.add('collapsed');
|
||||
const ov = document.getElementById('sidebarOverlay');
|
||||
@@ -377,15 +446,13 @@ function startRenameChat(chatId) {
|
||||
// ── Send Message ─────────────────────────────
|
||||
|
||||
async function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const text = input.value.trim();
|
||||
const text = ChatInput.getValue().trim();
|
||||
const hasAttachments = hasStagedAttachments();
|
||||
|
||||
// Need text or attachments, not generating, not blocked by uploads
|
||||
if ((!text && !hasAttachments) || App.isGenerating || isSendBlocked()) return;
|
||||
|
||||
input.value = '';
|
||||
input.style.height = 'auto';
|
||||
ChatInput.setValue('');
|
||||
|
||||
// Snapshot attachment IDs before clearing staged state
|
||||
const attachmentIds = getStagedAttachmentIds();
|
||||
@@ -749,16 +816,8 @@ function _initChatListeners() {
|
||||
UI.toast(`Loaded ${visible} model${visible !== 1 ? 's' : ''}`, 'success');
|
||||
});
|
||||
|
||||
// Input
|
||||
const input = document.getElementById('messageInput');
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
||||
});
|
||||
input.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
|
||||
updateInputTokens();
|
||||
});
|
||||
// Input — CM6 or textarea fallback
|
||||
ChatInput.init();
|
||||
|
||||
// Close modals on overlay click
|
||||
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
|
||||
@@ -272,6 +272,11 @@ const DebugLog = {
|
||||
snap.extensions = Extensions.debug();
|
||||
}
|
||||
|
||||
// CM6 editor state
|
||||
snap.cm6 = window.CM
|
||||
? { version: window.CM.version, languages: Object.keys(window.CM.languages || {}) }
|
||||
: { available: false };
|
||||
|
||||
return snap;
|
||||
},
|
||||
|
||||
|
||||
@@ -69,8 +69,7 @@ function updateInputTokens() {
|
||||
const el = document.getElementById('inputTokenCount');
|
||||
if (!el) return;
|
||||
|
||||
const input = document.getElementById('messageInput');
|
||||
const inputText = input?.value || '';
|
||||
const inputText = (typeof ChatInput !== 'undefined') ? ChatInput.getValue() : '';
|
||||
const inputTokens = Tokens.estimate(inputText);
|
||||
|
||||
if (!inputText.trim()) {
|
||||
|
||||
@@ -11,17 +11,30 @@ Object.assign(UI, {
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
const scale = prefs.scale || 100;
|
||||
const msgFont = prefs.msgFont || 14;
|
||||
const theme = prefs.theme || 'dark';
|
||||
const keymap = prefs.editorKeymap || 'standard';
|
||||
|
||||
const scaleEl = document.getElementById('settingsScale');
|
||||
const msgFontEl = document.getElementById('settingsMsgFont');
|
||||
if (scaleEl) { scaleEl.value = scale; document.getElementById('scaleValue').textContent = scale + '%'; }
|
||||
if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; }
|
||||
|
||||
// Highlight active theme button
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.theme === theme);
|
||||
});
|
||||
|
||||
// Highlight active keymap button
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.keymap === keymap);
|
||||
});
|
||||
},
|
||||
|
||||
initAppearance() {
|
||||
// Load saved prefs on startup
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
UI.applyAppearance(prefs.scale || 100, prefs.msgFont || 14);
|
||||
UI.applyTheme(prefs.theme || 'dark');
|
||||
|
||||
// Live preview on slider change
|
||||
const scaleEl = document.getElementById('settingsScale');
|
||||
@@ -36,6 +49,70 @@ Object.assign(UI, {
|
||||
document.getElementById('msgFontValue').textContent = v + 'px';
|
||||
UI.applyAppearance(parseInt(scaleEl?.value || 100), v);
|
||||
});
|
||||
|
||||
// Theme toggle buttons
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const theme = btn.dataset.theme;
|
||||
UI.applyTheme(theme);
|
||||
document.querySelectorAll('#themeToggle .theme-btn').forEach(b =>
|
||||
b.classList.toggle('active', b === btn)
|
||||
);
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
p.theme = theme;
|
||||
localStorage.setItem('cs-appearance', JSON.stringify(p));
|
||||
});
|
||||
});
|
||||
|
||||
// Editor keymap toggle buttons
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.dataset.keymap;
|
||||
document.querySelectorAll('#keymapToggle .theme-btn').forEach(b =>
|
||||
b.classList.toggle('active', b === btn)
|
||||
);
|
||||
// Persist
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
p.editorKeymap = mode;
|
||||
localStorage.setItem('cs-appearance', JSON.stringify(p));
|
||||
// Notify open editors
|
||||
if (typeof Events !== 'undefined' && Events.emit) {
|
||||
Events.emit('keymap.changed', { mode });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// System theme change listener (for "system" mode)
|
||||
UI._systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
UI._systemThemeQuery.addEventListener('change', () => {
|
||||
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
if (p.theme === 'system') UI.applyTheme('system');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply theme to the document.
|
||||
* @param {'light'|'dark'|'system'} mode
|
||||
*/
|
||||
applyTheme(mode) {
|
||||
let resolved = mode;
|
||||
if (mode === 'system') {
|
||||
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
if (resolved === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
// Publish event for CM6 editors and extensions
|
||||
if (typeof Events !== 'undefined' && Events.emit) {
|
||||
Events.emit('theme.changed', { mode, resolved });
|
||||
}
|
||||
},
|
||||
|
||||
/** Get the currently resolved theme ('dark' or 'light') */
|
||||
getResolvedTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
||||
},
|
||||
|
||||
applyAppearance(scale, msgFont) {
|
||||
|
||||
Reference in New Issue
Block a user