Files
Vito 035e816db6 feat(ui): adopt shared toolkit for Prompt Manager Classic (#190)
* feat(ui): adopt shared toolkit for Prompt Manager Classic (#186)

* test(ui): retain Text node Web UI coverage alongside Classic

* fix(tooling): reject lookalike toolkit download URLs (#186)
2026-10-16 22:18:18 -07:00

157 lines
8.2 KiB
JavaScript

import { createScope, createButton, mountTheme } from './shared/toolkit/core.js';
function loadStyles() {
const href = new URL('./classic-node.css', import.meta.url).href;
if ([...document.querySelectorAll('link[rel="stylesheet"]')].some(link => link.href === href)) return;
const link = document.createElement('link');
Object.assign(link, { rel: 'stylesheet', href });
document.head.append(link);
}
function message(node, text, error = false) {
node._classicResultsScope?.dispose();
const element = document.createElement('p');
element.className = error ? 'pm-classic-message pm-classic-error' : 'pm-classic-message';
element.textContent = text;
element.setAttribute('role', error ? 'alert' : 'status');
node.resultsSection.replaceChildren(element);
}
export function installClassicControls(nodeType, app, parseSearchQuery, api) {
const prototype = nodeType.prototype;
const onCreated = prototype.onNodeCreated;
const onRemoved = prototype.onRemoved;
prototype.onNodeCreated = function (...args) {
this._classicScope = createScope();
this._classicNotifications = new Set();
this._classicScope.cleanup(() => {
clearTimeout(this._textSyncTimer);
clearTimeout(this.resultHideTimer);
this._classicResultsScope?.dispose();
for (const item of this._classicNotifications) { clearTimeout(item.timer); item.element.remove(); }
this._classicNotifications.clear();
});
onCreated.apply(this, args);
};
prototype.onRemoved = function (...args) {
this._classicScope?.dispose();
return onRemoved?.apply(this, args);
};
prototype.createSearchUI = function (container) {
loadStyles();
container.classList.add('ca-tools', 'pm-classic');
mountTheme(container, app.ui?.settings, this._classicScope);
this._classicContainer = container;
const wrapper = document.createElement('div');
wrapper.className = 'pm-classic-workspace';
const buttons = document.createElement('div');
buttons.className = 'pm-classic-toolbar';
for (const [label, action, primary] of [
['🔍 Search', () => this.performSearch(), true],
['📋 Recent', () => this.loadRecentPrompts()],
['🌐 Web UI', () => this.openWebInterface()],
]) {
const button = createButton(label, primary ? 'ca-primary' : '');
this._classicScope.listen(button, 'click', action);
buttons.append(button);
}
wrapper.append(buttons);
if (this.properties.showTestButton) {
const test = createButton('🔧 Test API');
this._classicScope.listen(test, 'click', () => this.testApiConnection());
wrapper.append(test);
}
this.resultsSection = document.createElement('div');
this.resultsSection.className = 'pm-classic-results';
wrapper.append(this.resultsSection);
container.replaceChildren(wrapper);
message(this, '💡 Click Search or Recent to find prompts');
};
prototype.performSearch = async function () {
const { text, tags } = parseSearchQuery(this.widgets?.find(widget => widget.name === 'search_text')?.value || '');
if (!text && !tags.length) { message(this, '⚠️ Enter search text or tag:name'); return; }
await this._classicQuery('search_prompts', { search_text: text, tags }, '🔍 Searching database...');
};
prototype.loadRecentPrompts = async function () {
await this._classicQuery('get_recent_prompts', { limit: 20 }, '📋 Loading recent prompts...');
};
prototype._classicQuery = async function (method, params, pending) {
message(this, pending);
try {
const response = await this.callNodeMethod(method, params);
if (this._classicScope.signal.aborted) return;
this.displayResults(response.results || []);
this.showNotification(response.results?.length ? `Found ${response.results.length} prompts` : 'No recent prompts found');
} catch (error) {
if (this._classicScope.signal.aborted) return;
message(this, `❌ ${error.message}`, true);
this.showNotification(error.message, 'error');
}
};
prototype.displayResults = function (results) {
if (this._classicScope.signal.aborted) return;
this._searchResults = results || [];
this._selectedPromptIndex = -1;
if (!results?.length) { message(this, '📭 No prompts found'); return; }
this._classicResultsScope?.dispose();
this._classicResultsScope = createScope();
const header = document.createElement('p');
header.className = 'pm-classic-count';
header.textContent = `📚 Found ${results.length} prompt${results.length === 1 ? '' : 's'}`;
this.resultsSection.replaceChildren(header);
results.forEach((result, index) => {
const button = createButton('', 'pm-classic-result');
button.setAttribute('aria-pressed', 'false');
const preview = document.createElement('span');
preview.className = 'pm-classic-preview';
preview.textContent = result.text.length > 80 ? result.text.substring(0, 80) + '...' : result.text;
const metadata = document.createElement('span');
metadata.className = 'pm-classic-metadata';
metadata.textContent = `${result.category || 'No category'} · ${result.rating ? '⭐' + result.rating : 'No rating'} · ${result.created_at ? new Date(result.created_at).toLocaleDateString() : 'Unknown'} · ${Array.isArray(result.tags) ? result.tags.join(', ') : 'No tags'}`;
button.append(preview, metadata);
this._classicResultsScope.listen(button, 'click', () => this.usePrompt(result, index));
this.resultsSection.append(button);
});
this.startAutoHideTimer();
};
prototype.highlightSelectedResult = function (index) {
this._selectedPromptIndex = index;
this.resultsSection.querySelectorAll('.pm-classic-result').forEach((element, i) => element.setAttribute('aria-pressed', String(i === index)));
};
prototype.testApiConnection = async function () {
message(this, '🔧 Testing API connection...');
try {
const response = await fetch(api.fileURL('/prompt_manager/test'), { signal: this._classicScope.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
const data = await response.json();
if (!data.success) throw new Error(data.message || 'Test failed');
if (this._classicScope.signal.aborted) return;
message(this, `✅ API Connection Successful! ${data.message || ''} ${data.timestamp || ''}`);
this.showNotification('API connection test passed!', 'success');
} catch (error) {
if (this._classicScope.signal.aborted) return;
message(this, `❌ API Connection Failed: ${error.message}`, true);
this.showNotification(`API test failed: ${error.message}`, 'error');
}
};
prototype.hideResults = function () {
if (this._classicScope.signal.aborted || this.restoreButton) return;
this.resultsSection.style.display = 'none';
this.restoreButton = createButton('🔄 Results auto-hidden · Click to restore', 'pm-classic-restore');
this._classicScope.listen(this.restoreButton, 'click', () => this.showResults());
this.resultsSection.after(this.restoreButton);
};
prototype.showNotification = function (text, type = 'info') {
if (this._classicScope.signal.aborted) return;
const element = document.createElement('div');
element.className = 'ca-tools pm-classic-notification';
element.dataset.caTheme = this._classicContainer.dataset.caTheme;
element.dataset.kind = type;
element.setAttribute('role', type === 'error' ? 'alert' : 'status');
element.textContent = text;
document.body.append(element);
const item = { element, timer: setTimeout(() => { element.remove(); this._classicNotifications.delete(item); }, 3000) };
this._classicNotifications.add(item);
};
}