PageSourceSearch

https://droprompt.com/_static/cd603bf35fdbcf40f9f52119/assets/dp-template-suggest.js?v=3

js droprompt.com collected 2026-09-24 14:42:10 UTC 12,750 bytes, 258 lines download raw bytes

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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 = '&times;';
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.