deploy ProspectOS workspace v5 final

This commit is contained in:
Marco0300
2026-09-07 10:23:38 +02:00
parent e27a003703
commit 181b83fc5e
8 changed files with 649 additions and 17 deletions
+97
View File
@@ -150,3 +150,100 @@ function addMockupPanel(id){const body=$('drawerBody');const p=prospects.find(x=
async function generateMockupPreview(id,button,mode='onepage'){const status=$('mockupStatus');const chooser=document.querySelector('.mockup-mode-chooser');if(button)button.disabled=true;if(chooser)chooser.querySelectorAll('button').forEach(item=>item.disabled=true);if(status){status.textContent=`Generating ${mode==='multipage'?'multi-page':'one-page'} preview… The current public website may take a moment to read.`;status.className='notice'}try{const data=await api(`/api/prospects/${id}/mockup`,{method:'POST',body:JSON.stringify({instructions:'',mode})});if(status){status.innerHTML=`Preview ready (${esc(data.mode==='multipage'?'multiple pages':'one page')}). <a href="${esc(data.share_url)}" target="_blank" rel="noopener">Open share link ↗</a>`;status.className='notice ok'}const list=$('mockupsList');if(list){const latest=await api(`/api/prospects/${id}/mockups`);list.innerHTML=renderMockupItems(latest.items||[])}return data}catch(error){if(status){status.textContent=error.message;status.className='notice error'}throw error}finally{if(button)button.disabled=false;if(chooser)chooser.querySelectorAll('button').forEach(item=>item.disabled=false)}}
async function loadMessageMockupOption(id){const context=$('messageContext');if(!context)return;context.querySelector('.mockup-option')?.remove();try{const data=await api(`/api/prospects/${id}/mockups`);const latest=(data.items||[])[0];if(!latest)return;const option=document.createElement('label');option.className='mockup-option';option.innerHTML=`<input type="checkbox" id="includeMockup" data-mockup-id="${esc(latest.id)}" checked><span><b>Include latest concept preview</b><small>Share the editable preview link once in the email. <a href="${esc(latest.share_url)}" target="_blank" rel="noopener">Open it ↗</a></small></span>`;context.append(option)}catch(error){logAction('mockup_option_load_failed',{prospect_id:id,error:error.message},'warn')}}
document.addEventListener('click',async e=>{const generate=e.target.closest('[data-generate-mockup]');if(generate){e.preventDefault();try{await generateMockupPreview(generate.dataset.generateMockup,generate,generate.dataset.mockupMode||'onepage')}catch{}return}const copy=e.target.closest('[data-copy-mockup]');if(copy){e.preventDefault();try{await navigator.clipboard.writeText(copy.dataset.copyMockup);copy.textContent='Copied';setTimeout(()=>{copy.textContent='Copy link'},1400)}catch{prompt('Copy preview link',copy.dataset.copyMockup)}}});
/* Keep the mockup generation timeout visible with the other runtime controls. */
const _renderSettingsWithMockupTimeout=renderSettings;
renderSettings=function(){
_renderSettingsWithMockupTimeout();
const websiteTimeout=$('websiteTimeout');
const group=websiteTimeout?.closest('.setting-group');
if(!group||$('aiMockupTimeout'))return;
const label=document.createElement('label');
label.innerHTML=`AI mockup copy timeout (seconds)<input id="aiMockupTimeout" type="number" min="20" max="300" step="5" value="${Number(settings.limits?.ai_mockup_timeout_seconds||45)}"><small>After this wait the complete premium fallback renders automatically.</small>`;
group.append(label);
};
/* Capture this form before the legacy handler so the new value is saved in the same request. */
document.addEventListener('submit',async e=>{
if(e.target?.id!=='runtimeSettingsForm')return;
e.preventDefault();e.stopImmediatePropagation();
const message=$('runtimeSettingsMessage');
if(message){message.textContent='Saving…';message.className='notice'}
try{
const result=await api('/api/settings',{method:'POST',body:JSON.stringify({
browser_use:{enabled:$('browserEnabled').checked,max_steps:Number($('browserSteps').value),timeout_seconds:Number($('browserTimeout').value),llm_timeout_seconds:Number($('browserLlmTimeout').value)},
maps:{request_timeout_seconds:Number($('mapsTimeout').value),poll_seconds:Number($('mapsPoll').value),max_depth:Number($('mapsDepth').value)},
limits:{website_scan_timeout_seconds:Number($('websiteTimeout').value),ai_mockup_timeout_seconds:Number($('aiMockupTimeout')?.value||45)}
})});
settings.browser_use=result.browser_use||settings.browser_use;settings.maps={...settings.maps,...(result.maps||{})};settings.limits={...settings.limits,...(result.limits||{})};
if(message){message.textContent='Runtime settings saved.';message.className='notice ok'}
}catch(error){if(message){message.textContent=error.message;message.className='notice error'}}
},true);
/* AI template editor ----------------------------------------------------- */
/*
* Keep the preview in an iframe and the conversation in the parent app.
* The public preview itself is static and CSP/sandboxed, so an AI request
* can never inject a chat script, form handler, or tracking pixel into the
* share link.
*/
renderMockupItems=function(items=[]){
if(!items.length)return '<p class="muted">No preview yet. Generate one from the public facts already collected for this prospect.</p>';
return `<div class="mockup-list">${items.map(item=>`<div class="mockup-card"><div class="mockup-card-main"><b>${esc(item.title||'Website concept preview')}</b><small>${esc(item.mode==='multipage'?'Multi-page site':'One-page site')} · ${esc(item.generation_source==='ai'?'AI-generated':'Instant fallback')} · version ${Number(item.version||1)} · ${date(item.created_at)}</small>${item.pages?.length>1?`<span class="muted">${item.pages.length} pages · ${item.pages.map(page=>`<a href="${esc(page.url)}" target="_blank" rel="noopener">${esc(page.slug)}</a>`).join(' · ')}</span>`:''}${item.source_url?`<span class="muted">Based on ${esc(item.source_url)}</span>`:''}${Number(item.version||1)>1?`<span class="muted">Edited from an earlier version${item.edit_instructions?`: ${esc(item.edit_instructions)}`:''}</span>`:''}</div><div class="mockup-card-actions"><button type="button" class="primary" data-edit-mockup="${esc(item.id)}" data-edit-prospect="${esc(item.prospect_id)}" data-edit-share="${esc(item.share_url)}" data-edit-mode="${esc(item.mode||'onepage')}" data-edit-version="${esc(item.version||1)}">Edit template</button><a class="secondary mockup-link" href="${esc(item.share_url)}" target="_blank" rel="noopener">Open preview ↗</a><button type="button" class="secondary" data-copy-mockup="${esc(item.share_url)}">Copy link</button></div></div>`).join('')}</div>`;
};
function editorPageOptions(mode){
return mode==='multipage'?'<option value="all">Entire site</option><option value="home">Home page</option><option value="services">Services page</option><option value="about">About page</option><option value="contact">Contact page</option>':'<option value="all">Entire page</option>';
}
function editorPreviewUrl(url,refresh=''){
const value=String(url||'');if(!value)return '';
const join=value.includes('?')?'&':'?';return `${value}${join}embed=1${refresh?`&refresh=${encodeURIComponent(refresh)}`:''}`;
}
function editorAddMessage(messages,kind,text){
if(!messages)return;
const bubble=document.createElement('div');bubble.className=`mockup-chat-message ${kind||'assistant'}`;bubble.textContent=String(text||'');messages.append(bubble);messages.scrollTop=messages.scrollHeight;
}
async function openMockupEditor(button){
const id=button?.dataset?.editMockup;
if(!id)return;
document.getElementById('mockupEditorDialog')?.remove();
let state={id:Number(id),prospect_id:Number(button.dataset.editProspect||0),share_url:button.dataset.editShare||'',mode:button.dataset.editMode||'onepage',version:Number(button.dataset.editVersion||1),title:'Website concept preview'};
const dialog=document.createElement('dialog');dialog.id='mockupEditorDialog';dialog.className='mockup-editor-dialog';
dialog.innerHTML=`<div class="mockup-editor-shell"><header class="mockup-editor-head"><div><span class="eyebrow">TEMPLATE EDITOR</span><h2>${esc(state.title)}</h2><p>Ask the AI for a focused change, then review the new version before sharing it.</p></div><div class="mockup-editor-head-actions"><span id="mockupEditorVersion" class="pill">Version ${state.version}</span><button type="button" class="icon-btn" data-editor-close aria-label="Close editor">×</button></div></header><div class="mockup-editor-layout"><section class="mockup-editor-preview"><div class="mockup-editor-preview-bar"><b>Live concept preview</b><a id="mockupEditorOpen" href="${esc(state.share_url)}" target="_blank" rel="noopener">Open full preview ↗</a></div><iframe id="mockupEditorFrame" title="Website concept preview" sandbox="allow-same-origin" referrerpolicy="no-referrer" src="${esc(editorPreviewUrl(state.share_url))}"></iframe></section><aside class="mockup-editor-chat"><div class="mockup-chat-head"><div><span class="eyebrow">AI DESIGN CHAT</span><h3>What should change?</h3></div><span class="mockup-chat-badge">Safe renderer</span></div><div id="mockupEditorMessages" class="mockup-chat-messages"></div><form id="mockupEditorForm" class="mockup-chat-form"><label>Apply to<select id="mockupEditorPage">${editorPageOptions(state.mode)}</select></label><textarea id="mockupEditorInput" rows="5" required maxlength="3000" placeholder="e.g. Make the design darker, make Services more prominent, and change the headline to something more confident."></textarea><div id="mockupEditorStatus" class="notice">The AI uses the prospect evidence and your instruction. It will not add scripts, forms, tracking, or unsupported claims.</div><button class="primary" type="submit" id="mockupEditorApply">Apply change <span>→</span></button></form><div class="mockup-editor-note"><b>Version history is preserved</b><span>Every applied change creates a new share link. The previous version remains available in the prospect drawer.</span></div></aside></div></div>`;
document.body.append(dialog);
const close=()=>{try{dialog.close()}catch{}dialog.remove()};
dialog.querySelector('[data-editor-close]')?.addEventListener('click',close);
dialog.addEventListener('cancel',e=>{e.preventDefault();close()});
dialog.addEventListener('click',e=>{if(e.target===dialog)close()});
try{dialog.showModal()}catch{dialog.setAttribute('open','')}
const messages=$('mockupEditorMessages'),frame=$('mockupEditorFrame'),openLink=$('mockupEditorOpen'),versionLabel=$('mockupEditorVersion'),form=$('mockupEditorForm'),input=$('mockupEditorInput'),status=$('mockupEditorStatus'),apply=$('mockupEditorApply');
editorAddMessage(messages,'assistant','I can refine the layout, copy, palette, emphasis, and imagery while keeping the concept grounded in the public prospect evidence. What would you like changed?');
try{
const remote=await api(`/api/mockups/${state.id}`);state={...state,...remote};
const editorTitle=dialog.querySelector('.mockup-editor-head h2');if(editorTitle)editorTitle.textContent=state.title||'Website concept preview';
if(frame&&state.share_url)frame.src=editorPreviewUrl(state.share_url);if(openLink)openLink.href=state.share_url||'#';if(versionLabel)versionLabel.textContent=`Version ${Number(state.version||1)}`;
}catch(error){editorAddMessage(messages,'system',`Could not load editor metadata: ${error.message}. The preview can still be edited.`)}
form?.addEventListener('submit',async event=>{
event.preventDefault();
const instruction=String(input?.value||'').trim();if(instruction.length<3){if(status){status.textContent='Tell the AI what you would like changed.';status.className='notice error'}return}
const page=$('mockupEditorPage')?.value||'all';
if(apply)apply.disabled=true;if(input)input.disabled=true;if(status){status.textContent='Applying your request and rendering a new version…';status.className='notice'}
editorAddMessage(messages,'user',instruction);
try{
const result=await api(`/api/mockups/${state.id}/edit`,{method:'POST',body:JSON.stringify({instruction,page})});
state={...state,...result};const editorTitle=dialog.querySelector('.mockup-editor-head h2');if(editorTitle)editorTitle.textContent=state.title||'Website concept preview';editorAddMessage(messages,'assistant',`${result.change_summary||'Your change was applied.'} Version ${Number(result.version||1)} is ready to review.`);
if(frame&&result.share_url)frame.src=editorPreviewUrl(result.share_url,Date.now());if(openLink)openLink.href=result.share_url||'#';if(versionLabel)versionLabel.textContent=`Version ${Number(result.version||1)}`;if(input)input.value='';if(status){status.textContent='New version ready. Review the preview, then ask for another change or copy the share link.';status.className='notice ok'}
if(state.prospect_id){try{const latest=await api(`/api/prospects/${state.prospect_id}/mockups`);const list=$('mockupsList');if(list)list.innerHTML=renderMockupItems(latest.items||[])}catch{}}
}catch(error){editorAddMessage(messages,'system',`I could not apply that change: ${error.message}`);if(status){status.textContent=error.message;status.className='notice error'}}finally{if(apply)apply.disabled=false;if(input)input.disabled=false;input?.focus()}
});
input?.focus();
}
/* Capture before row-level click handlers so opening the editor does not
also open/close the prospect drawer underneath it. */
document.addEventListener('click',e=>{
const edit=e.target.closest('[data-edit-mockup]');
if(!edit)return;
e.preventDefault();e.stopPropagation();openMockupEditor(edit);
},true);