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);
+2 -2
View File
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0d1726"><title>ProspectOS</title><link rel="stylesheet" href="/style.css?v=workspace-v2"></head>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><aside id="detailDrawer" class="detail-drawer" aria-hidden="true"><div class="drawer-head"><div><span class="eyebrow">PROSPECT DETAIL</span><h2 id="drawerName">Prospect</h2></div><button class="icon-btn" id="closeDrawer">×</button></div><div id="drawerBody"></div></aside><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">AI ASSISTED DRAFT</label><h2 id="messageTitle">Tailor a message</h2><p>Use the evidence and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate subject and email</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=workspace-v2"></script></body></html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#0d1726"><title>ProspectOS</title><link rel="stylesheet" href="/style.css?v=workspace-v5"></head>
<body><div class="app-shell"><aside class="sidebar"><div class="brand"><span class="brand-mark">P</span><div><b>ProspectOS</b><small>Growth workspace</small></div></div><nav id="nav"><a href="#dashboard" data-route="dashboard"><span>⌂</span>Dashboard</a><a href="#discover" data-route="discover"><span>⌁</span>Discover</a><a href="#prospects" data-route="prospects"><span>▦</span>Prospects</a><a href="#runs" data-route="runs"><span>◷</span>Runs</a><a href="#settings" data-route="settings"><span>⚙</span>Settings</a></nav><div class="sidebar-foot"><span class="status-dot" id="sourceDot"></span><div><b id="sourceLabel">Checking worker</b><small id="sourceSub">Connecting…</small></div></div></aside><main class="main"><header class="topbar"><div><span class="crumb">Workspace</span><span class="slash">/</span><b id="pageName">Dashboard</b></div><div class="top-actions"><span id="updated">—</span><button class="icon-btn" id="refreshAll" title="Refresh">↻</button></div></header><div id="view"></div><footer>ProspectOS · public data only · manual review before contact</footer></main></div><aside id="detailDrawer" class="detail-drawer" aria-hidden="true"><div class="drawer-head"><div><span class="eyebrow">PROSPECT DETAIL</span><h2 id="drawerName">Prospect</h2></div><button class="icon-btn" id="closeDrawer">×</button></div><div id="drawerBody"></div></aside><dialog id="messageDialog"><form method="dialog" id="messageForm"><div class="dialog-head"><div><label class="eyebrow">AI ASSISTED DRAFT</label><h2 id="messageTitle">Tailor a message</h2><p>Use the evidence and your offer to create an editable draft.</p></div><button class="icon-btn" value="cancel">×</button></div><label for="offerInput">Your offer or instruction</label><textarea id="offerInput" rows="4" placeholder="Move hosting to Gold Plus for R119/month and rebuild the website free."></textarea><div id="messageStatus" class="notice"></div><button class="primary" id="generateMessage" type="button">Generate subject and email</button><label for="subjectOutput">Subject</label><input id="subjectOutput"><label for="bodyOutput">Email draft</label><textarea id="bodyOutput" rows="12"></textarea><div class="dialog-actions"><button class="secondary" id="copyMessage" type="button">Copy draft</button><button class="secondary" value="cancel">Close</button></div></form></dialog><script src="/app.js?v=workspace-v5"></script></body></html>
+14
View File
@@ -158,3 +158,17 @@ input::placeholder { color: #aeb6c5; } input:focus, select:focus { border-color:
@media(max-width:680px){.mockup-card{display:block}.mockup-card-actions{margin-top:9px;justify-content:flex-start}.mockup-card-actions a,.mockup-card-actions button{flex:1}.mockup-panel .primary{width:100%}}
.mockup-mode-chooser{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin:12px 0}.mockup-mode-chooser button{display:grid;justify-items:start;gap:3px;text-align:left;padding:11px 12px}.mockup-mode-chooser button b{font-size:11px}.mockup-mode-chooser button small{font-size:10px;font-weight:500;opacity:.8}.mockup-mode-chooser .secondary{color:#48556c}
@media(max-width:560px){.mockup-mode-chooser{grid-template-columns:1fr}.mockup-mode-chooser button{width:100%}}
/* AI template editor */
.mockup-editor-dialog{width:min(1240px,calc(100vw - 28px));height:min(860px,calc(100vh - 28px));max-width:none;max-height:none;padding:0;border:0;border-radius:18px;background:#f8f9fd;color:#27344b;box-shadow:0 30px 100px #18233d45;overflow:hidden}
.mockup-editor-dialog::backdrop{background:#111a2dcc;backdrop-filter:blur(5px)}
.mockup-editor-shell{display:grid;grid-template-rows:auto minmax(0,1fr);height:100%;min-height:0}
.mockup-editor-head{display:flex;align-items:flex-start;justify-content:space-between;gap:18px;padding:20px 24px;border-bottom:1px solid #e0e5ef;background:#fff}
.mockup-editor-head h2{margin:5px 0 4px;font-size:20px;letter-spacing:-.035em}.mockup-editor-head p{margin:0;color:#7b879a;font-size:11px}.mockup-editor-head-actions{display:flex;align-items:center;gap:10px}.mockup-editor-head-actions .icon-btn{flex:0 0 auto}
.mockup-editor-layout{display:grid;grid-template-columns:minmax(0,1fr) 370px;min-height:0}
.mockup-editor-preview{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;background:#e9edf5}.mockup-editor-preview-bar{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:11px 17px;border-bottom:1px solid #dce2ed;background:#f7f8fc;color:#5a687d;font-size:11px}.mockup-editor-preview-bar a{color:#5b50d3;text-decoration:none;font-weight:800;white-space:nowrap}.mockup-editor-preview iframe{display:block;width:100%;height:100%;min-height:360px;border:0;background:#fff}
.mockup-editor-chat{display:grid;grid-template-rows:auto minmax(0,1fr) auto auto;min-height:0;border-left:1px solid #dfe4ed;background:#fff}.mockup-chat-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;padding:19px 19px 13px;border-bottom:1px solid #edf0f5}.mockup-chat-head h3{margin:4px 0 0;font-size:16px;letter-spacing:-.025em}.mockup-chat-badge{padding:5px 8px;border-radius:999px;background:#eef8f5;color:#17876b;font-size:9px;font-weight:900;white-space:nowrap}
.mockup-chat-messages{display:grid;align-content:start;gap:10px;overflow:auto;padding:16px 16px 10px;background:linear-gradient(180deg,#fbfcff,#fff)}.mockup-chat-message{max-width:92%;padding:10px 11px;border-radius:11px;font-size:11px;line-height:1.5;white-space:pre-wrap}.mockup-chat-message.assistant{justify-self:start;border:1px solid #e1e5ef;background:#f5f6fb;color:#59677e}.mockup-chat-message.user{justify-self:end;background:#635bff;color:#fff;box-shadow:0 8px 18px #635bff2b}.mockup-chat-message.system{justify-self:start;border:1px solid #f1c7cd;background:#fff5f6;color:#a33e4d}
.mockup-chat-form{display:grid;gap:8px;padding:14px 16px 12px;border-top:1px solid #edf0f5}.mockup-chat-form label{display:grid;gap:5px;color:#627087;font-size:10px;font-weight:850}.mockup-chat-form select{margin:0;height:34px;font-size:11px}.mockup-chat-form textarea{margin:0;min-height:106px;resize:vertical;font-size:12px;line-height:1.5}.mockup-chat-form .notice{margin:0;font-size:10px;line-height:1.4}.mockup-chat-form button{width:100%;min-height:42px}.mockup-editor-note{display:grid;gap:4px;padding:12px 16px 16px;background:#fafbff;color:#8b96a8;font-size:10px;line-height:1.45}.mockup-editor-note b{color:#5c6a80;font-size:10px}.mockup-editor-note span{display:block}
@media(max-width:900px){.mockup-editor-dialog{width:min(760px,calc(100vw - 20px));height:min(900px,calc(100vh - 20px))}.mockup-editor-layout{grid-template-columns:1fr;grid-template-rows:minmax(310px,1fr) auto}.mockup-editor-preview iframe{min-height:300px}.mockup-editor-chat{grid-template-rows:auto minmax(95px,1fr) auto auto;border-left:0;border-top:1px solid #dfe4ed;max-height:430px}.mockup-chat-messages{min-height:95px;max-height:160px}}
@media(max-width:560px){.mockup-editor-dialog{width:100vw;height:100vh;border-radius:0}.mockup-editor-head{padding:15px 16px}.mockup-editor-head h2{font-size:17px}.mockup-editor-head p{max-width:230px}.mockup-editor-head-actions .pill{display:none}.mockup-editor-layout{grid-template-rows:minmax(260px,1fr) auto}.mockup-editor-preview-bar{padding:9px 12px}.mockup-editor-chat{max-height:430px}.mockup-chat-form{padding:11px 12px}.mockup-editor-note{padding:10px 12px 13px}}