1/* 2 * dp-template-suggest.js â "use a ready template, save credits" hint. 3 * 4 * As the user describes a request in any builder, this quietly searches the 5 * community feed + seeded library for a matching ready-made template of the 6 * SAME kind. When something close exists it slides a small card in under the 7 * input suggesting they reuse it instead of spending credits to generate. 8 * 9 * Each builder field declares its kind, so matches stay on-topic: 10 * #idea â prompt #wf-idea â workflow #au-prompt â automation 11 * #sk-idea â skill #ag-idea â agent [data-dp-suggest][data-dp-suggest-kind] 12 * 13 * Backend: GET /api/community/search?q=&kind=&limit= 14 */ 15(function () { 16 'use strict'; 17 if (window.dpTemplateSuggest) return; 18 19 var TARGETS = [ 20 { sel: '#idea', kind: 'prompt' }, 21 { sel: '#wf-idea', kind: 'workflow' }, 22 { sel: '#au-prompt', kind: 'automation' }, 23 { sel: '#sk-idea', kind: 'skill' }, 24 { sel: '#ag-idea', kind: 'agent' }, 25 ]; 26 27 var MIN_CHARS = 14; // wait for a real sentence before suggesting 28 var DEBOUNCE = 480; 29 var STYLE_ID = 'dp-ts-style'; 30 31 var KIND_LABEL = { 32 prompt: 'prompt', workflow: 'workflow', automation: 'automation', 33 agent: 'agent', skill: 'skill', 34 }; 35 36 var CSS = [ 37 '.dpts-host{margin:10px 0 0;box-sizing:border-box;width:100%;clear:both}', 38 '.dpts-host:empty{display:none}', 39 '.dpts-card{position:relative;box-sizing:border-box;width:100%;max-width:100%;overflow:hidden;border-radius:14px;padding:13px 14px 12px;', 40 'border:1px solid var(--border,rgba(0,0,0,.08));', 41 'background:color-mix(in srgb,var(--dp-blue,#008ffe) 5%,var(--surface,#fff));', 42 'box-shadow:var(--shadow-card,0 4px 16px rgba(15,17,23,.06));', 43 '-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);', 44 'animation:dpts-in .26s var(--ease,cubic-bezier(.2,.8,.2,1)) both}', 45 '@keyframes dpts-in{from{opacity:0;transform:translateY(-5px)}to{opacity:1;transform:none}}', 46 '.dpts-head{display:flex;align-items:flex-start;gap:9px;margin-bottom:9px}', 47 '.dpts-spark{flex:0 0 auto;width:24px;height:24px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;', 48 'background:var(--dp-blue,#008ffe);color:#fff;box-shadow:0 3px 10px var(--dp-blue-ring,rgba(0,143,254,.4))}', 49 '.dpts-spark svg{width:14px;height:14px}', 50 '.dpts-head-txt{flex:1;min-width:0}', 51 '.dpts-title{margin:0;font-size:13px;font-weight:700;color:var(--fg,#0f1117);line-height:1.3;letter-spacing:-.01em}', 52 '.dpts-sub{margin:1px 0 0;font-size:11.5px;color:var(--fg-muted,#6b7280);line-height:1.4}', 53 '.dpts-x{flex:0 0 auto;appearance:none;border:0;background:transparent;color:var(--fg-muted,#6b7280);cursor:pointer;', 54 'font-size:15px;line-height:1;padding:3px 5px;border-radius:7px;margin:-3px -4px 0 0}', 55 '.dpts-x:hover{background:rgba(127,127,127,.12);color:var(--fg,#111)}', 56 '.dpts-list{display:flex;flex-direction:column;gap:7px}', 57 '.dpts-item{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;min-width:0;max-width:100%;text-align:left;cursor:pointer;', 58 'border:1px solid var(--border,rgba(0,0,0,.08));border-radius:10px;padding:9px 10px;', 59 'background:var(--surface,#fff);transition:border-color .14s ease,box-shadow .14s ease,transform .12s ease;font:inherit}', 60 '.dpts-item:hover{border-color:var(--dp-blue,#008ffe);box-shadow:0 0 0 3px var(--dp-blue-ring,rgba(0,143,254,.16));transform:translateY(-1px)}', 61 '.dpts-item:focus-visible{outline:none;border-color:var(--dp-blue,#008ffe);box-shadow:0 0 0 3px var(--dp-blue-ring,rgba(0,143,254,.22))}', 62 '.dpts-it-main{flex:1;min-width:0}', 63 '.dpts-it-title{display:block;max-width:100%;font-size:12.5px;font-weight:650;color:var(--fg,#0f1117);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em}', 64 '.dpts-it-meta{display:block;max-width:100%;font-size:11px;color:var(--fg-muted,#6b7280);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}', 65 '.dpts-it-votes{flex:0 0 auto;display:inline-flex;align-items:center;gap:3px;font-size:11px;font-weight:700;color:var(--fg-muted,#6b7280)}', 66 '.dpts-it-votes svg{width:12px;height:12px}', 67 '.dpts-it-cta{flex:0 0 auto;font-size:11px;font-weight:700;color:var(--dp-blue,#008ffe);display:inline-flex;align-items:center;gap:3px}', 68 '.dpts-it-cta svg{width:13px;height:13px}', 69 '[dir="rtl"] .dpts-item,[dir="rtl"] .dpts-head{text-align:right}', 70 '@media (prefers-reduced-motion:reduce){.dpts-card{animation:none}.dpts-item:hover{transform:none}}', 71 ].join(''); 72 73 var SPARK = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l2.2 5.8L20 10l-5.8 2.2L12 18l-2.2-5.8L4 10l5.8-2.2z"/></svg>'; 74 var ARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>'; 75 var UP = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M6 11l6-6 6 6"/></svg>'; 76 77 function injectCss() { 78 if (document.getElementById(STYLE_ID)) return; 79 var s = document.createElement('style'); s.id = STYLE_ID; s.textContent = CSS; 80 (document.head || document.documentElement).appendChild(s); 81 } 82 function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); } 83 function toast(m, o) { if (window.dpToast) window.dpToast(m, o); } 84 function token() { try { return localStorage.getItem('dro_token') || ''; } catch (_) { return ''; } } 85 86 // A clean one-line subtitle. Some seeded rows stuffed an object or a raw JSON 87 // blob into `meta` (e.g. "[object Object]" or {"category":â¦}); never show that 88 // â fall back to a plain body snippet, and only if it isn't JSON either. 89 function looksJson(s) { var c = s.charAt(0); return c === '{' || c === '['; } 90 function cleanMeta(p) { 91 var m = p && p.meta; 92 if (m && typeof m === 'object') m = '';
93 m = String(m == null ? '' : m).replace(/\s+/g, ' ').trim(); 94 if (!m || looksJson(m)) { 95 var b = String(p && p.body != null ? p.body : '').replace(/\s+/g, ' ').trim(); 96 m = looksJson(b) ? '' : b; 97 } 98 return m.slice(0, 140); 99 } 100 101 // A loose significant-word count so we only suggest on a real description. 102 function significantEnough(q) { 103 var words = String(q || '').toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, ' ').split(/\s+/).filter(function (w) { 104 return w.length >= 3; 105 }); 106 return q.trim().length >= MIN_CHARS && words.length >= 2; 107 } 108 109 async function search(q, kind) { 110 var headers = {}; 111 var t = token(); if (t) headers.Authorization = 'Bearer ' + t; 112 var url = '/api/community/search?q=' + encodeURIComponent(q) + '&kind=' + encodeURIComponent(kind) + '&limit=4'; 113 var res = await fetch(url, { headers: headers, cache: 'no-store' }); 114 var payload = await res.json().catch(function () { return {}; }); 115 if (!res.ok || payload.success === false) return []; 116 return (payload.data && payload.data.posts) || []; 117 } 118 119 function Suggester(field, kind) { 120 this.field = field; this.kind = kind; 121 this.host = document.createElement('div'); 122 this.host.className = 'dpts-host'; 123 // Drop the card below the WHOLE input block â never inside .idea-wrap (which 124 // holds an absolutely-positioned counter + language hint) or inside a 125 // horizontal field row â so it can't collide or get squeezed. 126 var anchor = field.closest('.idea-wrap') 127 || field.closest('.au-prompt-row') 128 || field.closest('.wf-input-area') 129 || field.closest('.dpv-wrap') 130 || field; 131 if (anchor.parentNode) anchor.parentNode.insertBefore(this.host, anchor.nextSibling); 132 this.timer = null; this.lastQuery = ''; this.dismissed = ''; this.abort = null; 133 var self = this; 134 field.addEventListener('input', function () { self.onInput(); }); 135 } 136 137 Suggester.prototype.onInput = function () { 138 var self = this; 139 clearTimeout(this.timer); 140 var q = (this.field.value || '').trim(); 141 if (!significantEnough(q)) { this.clear(); return; } 142 if (q === this.dismissed) return; // don't re-nag a dismissed query 143 this.timer = setTimeout(function () { self.run(q); }, DEBOUNCE); 144 }; 145 146 Suggester.prototype.run = async function (q) { 147 if (q === this.lastQuery) return; 148 this.lastQuery = q; 149 try { 150 if (this.abort) this.abort.abort(); 151 } catch (_) {} 152 var posts; 153 try { posts = await search(q, this.kind); } 154 catch (_) { return; } 155 // The field may have changed while we waited. 156 if ((this.field.value || '').trim() !== q) return; 157 if (q === this.dismissed) return; 158 if (!posts || !posts.length) { this.clear(); return; } 159 this.render(posts.slice(0, 3), q); 160 }; 161 162 Suggester.prototype.clear = function () { this.host.innerHTML = ''; }; 163 164 Suggester.prototype.render = function (posts, q) { 165 var self = this; 166 this.clear(); 167 var card = document.createElement('div'); 168 card.className = 'dpts-card'; 169 card.setAttribute('role', 'region'); 170 card.setAttribute('aria-label', 'Ready-made template suggestions'); 171 172 var head = document.createElement('div'); 173 head.className = 'dpts-head'; 174 head.innerHTML = 175 '<span class="dpts-spark">' + SPARK + '</span>' + 176 '<div class="dpts-head-txt">' + 177 '<p class="dpts-title">Thereâs a ready ' + esc(KIND_LABEL[this.kind] || 'template') + ' like this</p>' + 178 '<p class="dpts-sub">Reuse a proven one from the library â no credits spent generating.</p>' + 179 '</div>'; 180 var x = document.createElement('button'); 181 x.type = 'button'; x.className = 'dpts-x'; x.setAttribute('aria-label', 'Dismiss'); 182 x.innerHTML = '×'; 183 x.addEventListener('click', function () { self.dismissed = q; self.clear(); }); 184 head.appendChild(x); 185 card.appendChild(head); 186 187 var list = document.createElement('div'); 188 list.className = 'dpts-list'; 189 posts.forEach(function (p) { 190 var item = document.createElement('button'); 191 item.type = 'button'; 192 item.className = 'dpts-item'; 193 var meta = cleanMeta(p); 194 var votes = Number(p.upvotes || p.score || 0); 195 item.innerHTML = 196 '<span class="dpts-it-main">' + 197 '<span class="dpts-it-title">' + esc(p.title || 'Untitled') + '</span>' + 198 (meta ? '<span class="dpts-it-meta">' + esc(meta) + '</span>' : '') + 199 '</span>' + 200 (votes > 0 ? '<span class="dpts-it-votes">' + UP + esc(votes) + '</span>' : '') + 201 '<span class="dpts-it-cta">' + (self.kind === 'prompt' ? 'Use' : 'Open') + ARROW + '</span>'; 202 item.addEventListener('click', function () { self.use(p); }); 203 list.appendChild(item); 204 }); 205 card.appendChild(list); 206 this.host.appendChild(card); 207 }; 208 209 Suggester.prototype.use = function (post) { 210 // Prompts are finished text â drop them straight into the field and copy, so 211 // the user can run with it without spending a generation. Graph/spec/doc 212 // kinds open their feed post, which carries the "Open in Builder" handoff. 213 if (this.kind === 'prompt' && post.body) { 214 this.field.value = post.body; 215 this.field.dispatchEvent(new Event('input', { bubbles: true }));
216 try { this.field.focus(); this.field.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (_) {} 217 if (window.dpCopy) { try { window.dpCopy(post.body, 'Template'); } catch (_) {} } 218 toast('Loaded a ready template â no credits used.', { type: 'success' }); 219 this.dismissed = this.lastQuery; 220 this.clear(); 221 return; 222 } 223 if (post.id != null) { 224 window.open('/feed/p/' + encodeURIComponent(post.id), '_blank', 'noopener'); 225 this.dismissed = this.lastQuery; 226 this.clear(); 227 } 228 }; 229 230 function attach(field, kind) { 231 if (!field || field.dataset.dptsBound) return; 232 field.dataset.dptsBound = '1'; 233 injectCss(); 234 new Suggester(field, kind); 235 } 236 237 function scan() { 238 TARGETS.forEach(function (t) { 239 document.querySelectorAll(t.sel).forEach(function (el) { attach(el, t.kind); }); 240 }); 241 document.querySelectorAll('[data-dp-suggest]').forEach(function (el) { 242 attach(el, el.getAttribute('data-dp-suggest-kind') || 'prompt'); 243 }); 244 } 245 246 function init() { 247 scan(); 248 try { 249 var mo = new MutationObserver(function () { scan(); }); 250 mo.observe(document.body, { childList: true, subtree: true }); 251 } catch (_) {} 252 } 253 254 window.dpTemplateSuggest = { attach: attach, scan: scan }; 255 256 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); 257 else init(); 258})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.