deploy Nous v21 update
This commit is contained in:
+18
-2
@@ -1,5 +1,10 @@
|
||||
const $=id=>document.getElementById(id); const esc=v=>String(v??"").replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',"'":'''}[c]));
|
||||
let prospects=[],runs=[],settings={},selected=null,pollTimer=null;
|
||||
let prospects=[],runs=[],settings={},logs=[],selected=null,pollTimer=null;
|
||||
let activeLogTab='all';
|
||||
const LOG_TABS=[['all','All'],['action','Actions'],['api','API calls'],['ai','AI calls'],['search','Searches'],['browser','Browser/tool steps'],['errors','Errors']];
|
||||
function logAction(action,details={},level='info'){try{fetch('/api/logs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,details,level,path:location.hash.slice(1)||'dashboard'})}).catch(()=>{})}catch{}}
|
||||
function installLogsNav(){const nav=$('nav');if(!nav||nav.querySelector('[data-route="logs"]'))return;const link=document.createElement('a');link.href='#logs';link.dataset.route='logs';link.innerHTML='<span>≋</span>Logs';nav.append(link)}
|
||||
installLogsNav();
|
||||
let activeProspectCategory='all';
|
||||
const HIGH_POTENTIAL_SCORE=70;
|
||||
async function api(path,opt={}){const r=await fetch(path,{...opt,headers:{'Content-Type':'application/json',...(opt.headers||{})}});const b=await r.json().catch(()=>({}));if(!r.ok)throw Error(b.error||`Request failed (${r.status})`);return b}
|
||||
@@ -15,9 +20,14 @@ function renderDashboard(){const total=prospects.length,pending=prospects.filter
|
||||
function renderDiscover(){ $('view').innerHTML=layout('Discover','Set the criteria for this run. Source configuration and provider settings live in Settings.',`<div class="two-col"><article class="panel form-panel"><div class="panel-title"><div><span class="eyebrow">NEW RUN</span><h2>Find potential customers</h2><p>Search Google Maps, deduplicate the catalogue, then enrich each result.</p></div><span class="pill">Email collection always on</span></div><form id="discoverForm"><label>Keywords<input id="keywords" required placeholder="plumbing services"></label><label>Category<input id="category" required placeholder="plumbers"></label><label>Location<input id="location" value="South Africa" required></label><label>Maximum results<select id="max-results"><option>10</option><option selected>25</option><option>50</option></select></label><label class="checkline"><input type="checkbox" id="extra-reviews"> Collect extra reviews <small>May increase run time</small></label><div class="form-row"><button class="primary" type="submit" id="discoverBtn">Run discovery →</button><span id="discoverMessage" class="notice"></span></div></form></article><article class="panel"><div class="panel-title"><div><span class="eyebrow">HOW IT WORKS</span><h2>Evidence-first pipeline</h2></div></div><ol class="steps"><li><b>Search</b><span>Maps worker finds public business listings.</span></li><li><b>Enrich</b><span>Website, HTTPS, DNS hints and public email checks run safely.</span></li><li><b>Review</b><span>You decide what is relevant before any contact.</span></li></ol></article></div>`)}
|
||||
function renderProspects(){ $('view').innerHTML=layout('Prospects','Browse the strongest opportunities first, then switch to a search category when you want a focused list.',`<article class="panel"><div class="toolbar"><div class="search"><span>⌕</span><input id="prospectSearch" placeholder="Search name, address, domain or email"></div><select id="prospectStatus"><option value="">All review statuses</option><option value="pending">Needs review</option><option value="approved">Approved</option><option value="rejected">Rejected</option><option value="contacted">Contacted</option></select><button class="secondary" id="exportCsv">Export CSV</button><button class="secondary" id="refreshProspects">Refresh</button></div><div id="prospectTabs" class="prospect-tabs">${prospectTabs()}</div><div id="prospectTable">${prospectTable()}</div></article><aside id="detail" class="detail-panel" hidden></aside>`)}
|
||||
function renderRuns(){ $('view').innerHTML=layout('Runs','Monitor discovery jobs and inspect errors without leaving the run history.',`<article class="panel"><div class="table-wrap"><table><thead><tr><th>Run</th><th>Status</th><th>Results</th><th>Started</th><th>Source</th></tr></thead><tbody>${runs.map(r=>`<tr><td><b>#${r.id}</b><small>${esc(r.query)}</small></td><td><span class="status ${r.status}">${esc(r.status)}</span><small>${esc(r.stage||'')}</small></td><td>${r.added||0} added / ${r.found||0} found</td><td>${date(r.created_at)}</td><td>${esc(r.source||'google_maps')}</td></tr>`).join('')}</tbody></table></div>${runs.length?'':empty('No discovery runs yet')}</article>`)}
|
||||
function logTabCount(tab,counts){return tab==='errors'?Number(counts?.errors||0):Number(counts?.[tab]||0)}
|
||||
function logTabs(counts={}){return `<div class="logs-tab-list">${LOG_TABS.map(([key,label])=>`<button class="logs-tab ${activeLogTab===key?'active':''}" data-log-tab="${key}">${label}<span>${logTabCount(key,counts)}</span></button>`).join('')}</div>`}
|
||||
function renderLogTable(){const rows=Array.isArray(logs)?logs:[];return rows.length?`<div class="table-wrap log-table-wrap"><table><thead><tr><th>Time</th><th>Type</th><th>Event</th><th>Status</th><th>Duration</th><th>Details</th></tr></thead><tbody>${rows.map(row=>{const details=typeof row.details==='string'?row.details:JSON.stringify(row.details||{},null,2);return `<tr><td>${esc(date(row.created_at))}</td><td><span class="log-kind ${esc(row.level||'info')}">${esc(row.kind||'system')}</span></td><td><b>${esc(row.event||'')}</b>${row.path?`<small>${esc(row.method||'')} ${esc(row.path)}</small>`:''}${row.error?`<small class="log-error">${esc(row.error)}</small>`:''}</td><td>${row.status?esc(row.status):'—'}</td><td>${row.duration_ms!=null?esc(row.duration_ms)+' ms':'—'}</td><td><details><summary>View</summary><pre>${esc(details)}</pre></details></td></tr>`}).join('')}</tbody></table></div>`:empty('No log entries for this tab')}
|
||||
async function refreshLogs(){const status=$('logsStatus');if(status)status.textContent='Loading logs…';try{const params=new URLSearchParams({limit:'250'});if(activeLogTab==='errors')params.set('level','error');else if(activeLogTab!=='all')params.set('kind',activeLogTab);const data=await api('/api/logs?'+params.toString());logs=data.items||[];const tabs=$('logsTabs');if(tabs)tabs.innerHTML=logTabs(data.counts||{});const table=$('logsTable');if(table)table.innerHTML=renderLogTable();if(status)status.textContent=`Showing ${logs.length} of ${data.total||logs.length} entries · credentials are redacted`; }catch(error){if(status)status.textContent=error.message;if($('logsTable'))$('logsTable').innerHTML=empty('Logs could not be loaded')}}
|
||||
function renderLogs(){ $('view').innerHTML=layout('Logs','Inspect user actions, API calls, AI requests, searches, Browser Use steps, and errors in one place.',`<article class="panel logs-panel"><div class="panel-title"><div><span class="eyebrow">OPERATIONS JOURNAL</span><h2>Detailed activity</h2><p>Requests and actions are timestamped. API keys, authorization headers, and passwords are never stored.</p></div><div class="head-actions"><button class="secondary" id="refreshLogs">Refresh</button></div></div><div id="logsTabs" class="logs-tabs">${logTabs()}</div><div id="logsStatus" class="notice">Loading logs…</div><div id="logsTable"></div></article>`);refreshLogs()}
|
||||
function renderSettings(){const ai=settings.ai||{};const sch=settings.schedules||[];const sup=settings.suppressions||[];const provider=ai.provider||'custom';const modelList=Array.isArray(ai.models)?ai.models:[];const selectedModel=ai.model||'';const options=modelList.map(m=>`<option value="${esc(m.id)}">${esc(m.label||m.id)}${m.free?' · free':''}</option>`).join('')+(selectedModel&&!modelList.some(m=>m.id===selectedModel)?`<option value="${esc(selectedModel)}">${esc(selectedModel)}</option>`:'');$('view').innerHTML=layout('Settings','Configure providers, automation and safety controls here. These settings are separate from daily discovery.',`<div class="settings-grid"><article class="panel"><div class="panel-title"><div><span class="eyebrow">SOURCE</span><h2>Google Maps worker</h2><p>The bundled standard gosom worker runs locally and does not require an API key.</p></div><span class="status ${settings.maps?.ready?'succeeded':'failed'}">${settings.maps?.ready?'Ready':'Not connected'}</span></div><div class="setting-line"><span>Connection</span><b>${esc(settings.maps?.worker_url||'Not configured')}</b></div><div class="setting-line"><span>Email extraction</span><b class="good-text">Always on · public pages only</b></div><div class="setting-line"><span>Website scan</span><b>8 second bounded timeout</b></div></article><article class="panel"><div class="panel-title"><div><span class="eyebrow">AI PROVIDER</span><h2>Draft assistant</h2><p>Used only when you click “Draft AI email”.</p></div><span class="status ${ai.configured?'succeeded':'failed'}">${ai.configured?'Configured':'Not configured'}</span></div><form id="settingsForm"><label>Provider<select id="aiProvider"><option value="nous" ${provider==='nous'?'selected':''}>Nous Portal · free models</option><option value="custom" ${provider!=='nous'?'selected':''}>Custom OpenAI-compatible</option></select></label><p class="field-help" id="aiProviderNote">${provider==='nous'?'Nous uses the official inference endpoint. OAuth is supported by Hermes CLI; this standalone app uses a server-side API key.':'Use any OpenAI-compatible /v1 endpoint.'}</p><label>Base URL<input id="aiBase" value="${esc(ai.base_url||'')}" ${provider==='nous'?'readonly':''} placeholder="https://provider.example/v1"></label><label>API key<input id="aiKey" type="password" placeholder="Leave blank to keep current key"></label><label>Model<input id="aiModel" list="nousModelList" value="${esc(selectedModel)}" placeholder="model-name"><datalist id="nousModelList">${options}</datalist></label><p class="field-help">Nous model availability can change. Refresh Settings to update the free catalog.</p><div class="form-row"><button class="primary" type="submit">Save AI settings</button><button class="secondary" id="testAi" type="button">Test connection</button><span id="settingsMessage" class="notice"></span></div></form></article><article class="panel wide-panel"><div class="panel-title"><div><span class="eyebrow">SCHEDULES</span><h2>Recurring discovery</h2><p>Run bounded searches automatically; all results still arrive as needs-review.</p></div></div><form id="scheduleForm" class="inline-form"><input id="sCat" placeholder="Category"><input id="sKey" placeholder="Keywords"><input id="sLoc" value="South Africa" placeholder="Location"><select id="sHours"><option value="24">Daily</option><option value="48">Every 2 days</option><option value="168">Weekly</option></select><button class="secondary">Add schedule</button></form><div class="list">${sch.map(s=>`<div><b>${esc(s.keywords)} · ${esc(s.category)}</b><span>${esc(s.location)} · every ${s.interval_hours}h · next ${date(s.next_run_at)}</span></div>`).join('')||'<p class="muted">No schedules configured.</p>'}</div></article><article class="panel wide-panel"><div class="panel-title"><div><span class="eyebrow">SAFETY</span><h2>Suppression centre</h2><p>Suppressed domains and emails are excluded before new prospects are saved.</p></div></div><form id="suppressionForm" class="inline-form"><select id="supKind"><option value="domain">Domain</option><option value="email">Email</option></select><input id="supValue" placeholder="example.co.za or info@example.co.za"><input id="supReason" placeholder="Reason"><button class="secondary">Suppress</button></form><div class="list">${sup.map(s=>`<div><b>${esc(s.value)}</b><span>${esc(s.kind)} · ${esc(s.reason||'manual')} · ${date(s.created_at)}</span></div>`).join('')||'<p class="muted">No suppressions configured.</p>'}</div></article></div>`)}
|
||||
async function load(){[prospects,runs,settings]=await Promise.all([api('/api/prospects').then(x=>x.items||[]),api('/api/runs').then(x=>x.items||[]),Promise.all([api('/api/settings'),api('/api/schedules'),api('/api/suppressions')]).then(([s,sch,sup])=>({...s,schedules:sch.items||[],suppressions:sup.items||[]}))]);}
|
||||
async function route(){const r=location.hash.slice(1)||'dashboard';document.querySelectorAll('[data-route]').forEach(a=>a.classList.toggle('active',a.dataset.route===r));try{await load();({dashboard:renderDashboard,discover:renderDiscover,prospects:renderProspects,runs:renderRuns,settings:renderSettings}[r]||renderDashboard)();$('updated').textContent='Updated '+new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});}catch(e){$('view').innerHTML=empty(e.message)}}
|
||||
async function route(){const r=location.hash.slice(1)||'dashboard';installLogsNav();document.querySelectorAll('[data-route]').forEach(a=>a.classList.toggle('active',a.dataset.route===r));try{await load();({dashboard:renderDashboard,discover:renderDiscover,prospects:renderProspects,runs:renderRuns,logs:renderLogs,settings:renderSettings}[r]||renderDashboard)();$('updated').textContent='Updated '+new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});}catch(e){$('view').innerHTML=empty(e.message)}}
|
||||
function suggestedOffer(p){if(p.ai_offer)return p.ai_offer;if(['broken','unreachable','error','slow'].includes(String(p.website_status||'')))return'Move their hosting to our Gold Plus package at R119/month and rebuild the website at no extra design charge.';if(!p.website)return'Offer a professional .co.za domain, reliable hosting from R119/month, professional email, and a free one-page website.';return'Offer a friendly website and hosting review; only suggest a change that is supported by the evidence.'}
|
||||
function jsonArray(value){try{const parsed=JSON.parse(value||'[]');return Array.isArray(parsed)?parsed:[]}catch{return[]}}
|
||||
function showDraftNotes(d){let notes=$('draftNotes');if(!notes){notes=document.createElement('div');notes.id='draftNotes';notes.className='draft-notes';const status=$('messageStatus');if(status)status.parentNode.insertBefore(notes,status)}if(!notes)return;const unknowns=Array.isArray(d?.unknowns)?d.unknowns:[];const used=Array.isArray(d?.evidence_used)?d.evidence_used:[];notes.innerHTML=`<b>AI review notes</b><p>${esc(d?.rationale||'The draft uses the prospect evidence and your instructions.')}</p>${unknowns.length?`<p><b>Confirm before sending:</b> ${esc(unknowns.join(' · '))}</p>`:''}${used.length?`<p class="muted">Evidence used: ${esc(used.join(', '))}</p>`:''}${d?.follow_up?`<p class="muted">${esc(d.follow_up)}</p>`:''}`}
|
||||
@@ -39,3 +49,9 @@ async function findWebsiteCandidates(id,button){if(button){button.disabled=true;
|
||||
document.addEventListener('click',async e=>{const find=e.target.closest('[data-find-websites]');if(find){e.preventDefault();e.stopPropagation();await findWebsiteCandidates(find.dataset.findWebsites,find);return}const accept=e.target.closest('[data-accept-website]');if(accept){e.preventDefault();e.stopPropagation();accept.disabled=true;accept.textContent='Checking…';try{const d=await api(`/api/prospects/${accept.dataset.prospectId}/website-candidates/${accept.dataset.acceptWebsite}`,{method:'PATCH',body:JSON.stringify({status:'accepted'})});if(d.prospect){const i=prospects.findIndex(p=>String(p.id)===String(d.prospect.id));if(i>=0)prospects[i]={...prospects[i],...d.prospect};selected=d.prospect}openDrawer(accept.dataset.prospectId);if($('prospectTable'))$('prospectTable').innerHTML=prospectTable()}catch(error){accept.disabled=false;accept.textContent='Accept domain';const panel=$('websiteCandidatesPanel');if(panel){const notice=document.createElement('p');notice.className='notice error';notice.textContent=error.message;panel.append(notice)}}return}const reject=e.target.closest('[data-reject-website]');if(reject){e.preventDefault();e.stopPropagation();reject.disabled=true;try{const d=await api(`/api/prospects/${reject.dataset.prospectId}/website-candidates/${reject.dataset.rejectWebsite}`,{method:'PATCH',body:JSON.stringify({status:'rejected'})});const current=await api(`/api/prospects/${reject.dataset.prospectId}/website-candidates`);renderWebsiteCandidates(current.items||[],reject.dataset.prospectId)}catch(error){reject.disabled=false;const panel=$('websiteCandidatesPanel');if(panel){const notice=document.createElement('p');notice.className='notice error';notice.textContent=error.message;panel.append(notice)}}}});
|
||||
window.addEventListener('hashchange',route);$('refreshAll').addEventListener('click',route);route();
|
||||
document.addEventListener('click',e=>{const tab=e.target.closest('[data-category-tab]');if(!tab)return;e.preventDefault();e.stopPropagation();activeProspectCategory=tab.dataset.categoryTab||'all';if($('prospectTabs'))$('prospectTabs').innerHTML=prospectTabs();if($('prospectTable'))$('prospectTable').innerHTML=prospectTable()});
|
||||
document.addEventListener('click',e=>{const tab=e.target.closest('[data-log-tab]');if(tab){e.preventDefault();activeLogTab=tab.dataset.logTab||'all';refreshLogs();return}if(e.target.id==='refreshLogs')refreshLogs()});
|
||||
document.addEventListener('click',e=>{const target=e.target.closest('button,a,select,input,textarea,tr,[role="button"]');if(!target)return;const details={tag:target.tagName.toLowerCase(),id:target.id||'',label:target.getAttribute('aria-label')||target.getAttribute('title')||'',route:location.hash.slice(1)||'dashboard'};for(const name of ['open','message','assess','findWebsites','acceptWebsite','rejectWebsite','categoryTab','logTab','review'])if(target.dataset[name])details[name]=target.dataset[name];logAction('ui_click',details)},true);
|
||||
document.addEventListener('change',e=>{const target=e.target;if(!target||!target.id&&!target.dataset.review)return;logAction('ui_change',{tag:target.tagName.toLowerCase(),id:target.id||'',review_id:target.dataset.review||'',route:location.hash.slice(1)||'dashboard'})},true);
|
||||
document.addEventListener('submit',e=>{if(e.target?.id)logAction('ui_submit',{form:e.target.id,route:location.hash.slice(1)||'dashboard'})},true);
|
||||
window.addEventListener('error',e=>logAction('client_error',{message:String(e.message||'Unknown browser error').slice(0,500),source:String(e.filename||'').slice(0,500),line:e.lineno||0},'error'));
|
||||
window.addEventListener('unhandledrejection',e=>logAction('client_unhandled_rejection',{message:String(e.reason?.message||e.reason||'Unhandled promise rejection').slice(0,500)},'error'));
|
||||
|
||||
+1
-1
@@ -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=ui-v16"></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=prospects-tabs-v1"></script></body></html>
|
||||
<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=logs-v1"></script></body></html>
|
||||
|
||||
@@ -106,6 +106,7 @@ input::placeholder { color: #aeb6c5; } input:focus, select:focus { border-color:
|
||||
|
||||
.setup-section { max-width: 1160px; display: grid; grid-template-columns: 1.2fr 1fr; gap: 30px; margin-top: 17px; padding: 22px 24px; border: 1px solid #e3e6ee; border-radius: 13px; background: #fbfcfe; }.setup-section h2 { margin: 5px 0; color: #38445a; font-size: 15px; }.setup-section p, .setup-section ol { margin: 6px 0 0; color: #8792a5; font-size: 11px; }.setup-section ol { padding-left: 17px; line-height: 1.9; }.setup-section code { padding: 2px 4px; border-radius: 4px; background: #eff1f6; color: #5d687d; font-size: 10px; }.footer { max-width: 1160px; display: flex; justify-content: space-between; gap: 20px; padding: 23px 2px 0; color: #a3acbb; font-size: 10px; }
|
||||
.muted { color: #9ca5b5; font-size: 11px; }
|
||||
.logs-panel{min-width:0}.logs-tabs{margin:22px 0 12px;padding:2px 0 0;border-bottom:1px solid #edf0f5}.logs-tab-list{display:flex;gap:5px;overflow-x:auto}.logs-tab{border:0;border-bottom:2px solid transparent;background:transparent;color:#718097;padding:11px 12px;margin-bottom:-1px;white-space:nowrap;font:inherit;font-size:11px;font-weight:800;cursor:pointer}.logs-tab:hover{color:#4f47df}.logs-tab.active{border-color:#635bff;color:#4f47df}.logs-tab span{display:inline-grid;place-items:center;min-width:19px;height:18px;margin-left:4px;padding:0 5px;border-radius:99px;background:#eef1f7;color:#68758a;font-size:9px}.logs-tab.active span{background:#eeecff;color:#574fe2}.log-table-wrap table{min-width:980px}.log-kind{display:inline-block;padding:5px 7px;border-radius:99px;background:#edf1f8;color:#5d6c83;font-size:9px;font-weight:800;text-transform:uppercase}.log-kind.error{background:#ffe7e9;color:#bd4253}.log-kind.warn{background:#fff2d7;color:#a16e17}.log-table-wrap td{vertical-align:top}.log-table-wrap td small{max-width:300px;overflow-wrap:anywhere}.log-error{color:#bd4253!important}.log-table-wrap details{max-width:370px}.log-table-wrap summary{color:#5b50d3;cursor:pointer;font-size:11px;font-weight:750}.log-table-wrap pre{max-height:240px;overflow:auto;margin:8px 0 0;padding:9px;border-radius:7px;background:#f6f7fb;color:#536178;font:10px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
||||
.draft-notes{padding:12px;border:1px solid #e6eaf2;border-radius:9px;background:#fafbff;color:#536178;font-size:11px;line-height:1.5}.draft-notes b{color:#334158}.draft-notes p{margin:4px 0}.draft-notes .muted{font-size:10px}
|
||||
.prospect-tabs{margin:4px 0 22px;padding:14px 0 0;border-top:1px solid #edf0f5}.prospect-tab-list{display:flex;gap:8px;overflow-x:auto;padding:2px 1px 3px}.prospect-tab{border:1px solid #dfe4ed;background:#fff;color:#536178;border-radius:999px;padding:9px 13px;white-space:nowrap;font:inherit;font-size:12px;font-weight:750;cursor:pointer;transition:.18s}.prospect-tab:hover{border-color:#b9b5ff;background:#f7f6ff;color:#4f47df}.prospect-tab.active{background:#635bff;border-color:#635bff;color:#fff;box-shadow:0 6px 14px #635bff2e}.prospect-tab span{display:inline-grid;place-items:center;min-width:20px;height:20px;margin-left:5px;padding:0 5px;border-radius:99px;background:#eef1f7;color:#647188;font-size:10px}.prospect-tab.active span{background:#fff3;color:#fff}.tab-hint{margin:10px 0 0;color:#8a96a8;font-size:11px}.candidate-list{display:grid;gap:10px;margin-top:15px}.candidate-card{padding:13px;border:1px solid #e6eaf2;border-radius:10px;background:#fbfcff}.candidate-card>div:first-child{display:flex;align-items:center;gap:8px}.candidate-card a{color:#4f47df;font-weight:750;text-decoration:none;font-size:13px}.candidate-card p{font-size:12px!important;color:#68758a!important}.candidate-card ul{padding-left:18px;margin:7px 0}.candidate-card li{font-size:11px!important;padding:2px 0!important}.candidate-actions{display:flex;gap:7px;margin-top:10px}.candidate-actions button{padding:8px 10px;font-size:11px}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user