architect: LP Objections page — UI trigger for the grounding pass (v0.1.0:66)
New admin "LP Objections" page (frontend ObjectionsPage + nav). Pick a segment (or All LPs) and Run grounding: the Architect mines matched LP emails + notes on the local model, scrubs every identifier through the redaction boundary, and asks Claude for the recurring objections + honest rebuttals (substantiated/hand-wavy flagged). Renders the de-identified draft + an "N identifiers protected" badge; fail-closed statuses (local_model_unavailable / scrub_unavailable / claude_not_configured / rehydrate_failed) show a clear message. Uses the existing /api/architect/ground route. Verified in preview: page + segment selector + Run; the local minimize/scrub legs actually ran against real Spark on synthetic input and fail-closed correctly at the (key-less) Claude step; success rendering verified with a mocked ok response. NOT yet deployed — start-cli RPC to the box hit a transient transport error post a StartOS hiccup (curl works, start-cli doesn't); CRM healthy at v0.1.0:65 meanwhile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9948,6 +9948,92 @@
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ObjectionsPage = ({ token, user, onShowToast }) => {
|
||||||
|
const isAdmin = user?.role === 'admin';
|
||||||
|
const [segment, setSegment] = useState('');
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
const [result, setResult] = useState(null);
|
||||||
|
const SEGMENTS = [
|
||||||
|
['', 'All LPs'],
|
||||||
|
['btc_native_hnwi', 'Bitcoin-native HNWIs'],
|
||||||
|
['institution', 'Institutions'],
|
||||||
|
['family_office', 'Family offices'],
|
||||||
|
['smaller_accredited', 'Smaller accredited ($100k)'],
|
||||||
|
['ai_energy_operator', 'AI & energy operators'],
|
||||||
|
];
|
||||||
|
const FAIL = {
|
||||||
|
local_model_unavailable: 'The local model (Spark) is unavailable, so nothing was sent to Claude. Try again once it is reachable.',
|
||||||
|
scrub_unavailable: 'The redaction boundary could not be prepared, so nothing was sent to Claude.',
|
||||||
|
claude_not_configured: 'The Architect (Claude) is not configured on the server.',
|
||||||
|
rehydrate_failed: 'Claude returned an unexpected placeholder; the draft was quarantined for safety. Nothing de-anonymized was shown.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
if (running) return;
|
||||||
|
try {
|
||||||
|
setRunning(true);
|
||||||
|
setResult(null);
|
||||||
|
const res = await api('/api/architect/ground', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(segment ? { segment_key: segment } : {}),
|
||||||
|
}, token);
|
||||||
|
setResult(res.data || res);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = getErrorMessage(err, 'Grounding failed');
|
||||||
|
setResult({ status: 'error', reason: msg });
|
||||||
|
onShowToast(msg, 'error');
|
||||||
|
} finally {
|
||||||
|
setRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAdmin) return <div className="empty-state">Admin only.</div>;
|
||||||
|
const ok = result && result.status === 'ok';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-container">
|
||||||
|
<h2 className="section-title" style={{ marginBottom: '20px' }}>LP Objections</h2>
|
||||||
|
<div className="section">
|
||||||
|
<div className="index-action-hint" style={{ marginTop: 0, marginBottom: '12px' }}>
|
||||||
|
The Architect reads your matched LP emails and notes on the local model, removes every identifier through the redaction boundary, and asks Claude for the recurring objections and the strongest honest rebuttals. Only de-identified themes ever leave Ten31 — no names, firms, amounts, or addresses. Results are a draft for your review.
|
||||||
|
</div>
|
||||||
|
<div className="index-action-buttons" style={{ alignItems: 'center' }}>
|
||||||
|
<select className="select-input" value={segment} onChange={(e) => setSegment(e.target.value)} style={{ maxWidth: '260px' }}>
|
||||||
|
{SEGMENTS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}
|
||||||
|
</select>
|
||||||
|
<button onClick={run} disabled={running}>
|
||||||
|
{running ? 'Grounding… (this can take a minute)' : 'Run grounding'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{running && <div className="section"><SkeletonBlock lines={6} /></div>}
|
||||||
|
|
||||||
|
{result && !running && (
|
||||||
|
<div className="section">
|
||||||
|
{ok ? (
|
||||||
|
<>
|
||||||
|
<div className="section-title" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
Recurring objections & rebuttals
|
||||||
|
{result.scrub_stats && result.scrub_stats.tokens != null
|
||||||
|
? <span className="approval-pill">{result.scrub_stats.tokens} identifiers protected</span> : null}
|
||||||
|
</div>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', fontFamily: 'inherit', lineHeight: 1.55, margin: 0 }}>{result.draft}</pre>
|
||||||
|
<div className="index-action-hint" style={{ marginTop: '12px' }}>
|
||||||
|
Draft for review. Use these to pressure-test the thesis in the Workshop.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="toast error" style={{ position: 'static' }}>
|
||||||
|
{FAIL[result.status] || result.reason || 'Grounding did not complete.'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const EmailCapturePage = ({ token, user, onShowToast }) => {
|
const EmailCapturePage = ({ token, user, onShowToast }) => {
|
||||||
const isAdmin = user?.role === 'admin';
|
const isAdmin = user?.role === 'admin';
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
@@ -10751,6 +10837,11 @@
|
|||||||
<button className={`nav-item ${page === 'thesis-workshop' ? 'active' : ''}`} onClick={() => setPage('thesis-workshop')}>
|
<button className={`nav-item ${page === 'thesis-workshop' ? 'active' : ''}`} onClick={() => setPage('thesis-workshop')}>
|
||||||
<span className="nav-item-icon">◆</span> Thesis Workshop
|
<span className="nav-item-icon">◆</span> Thesis Workshop
|
||||||
</button>
|
</button>
|
||||||
|
{user?.role === 'admin' && (
|
||||||
|
<button className={`nav-item ${page === 'objections' ? 'active' : ''}`} onClick={() => setPage('objections')}>
|
||||||
|
<span className="nav-item-icon">◇</span> LP Objections
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className={`nav-item ${page === 'system-status' ? 'active' : ''}`} onClick={() => setPage('system-status')}>
|
<button className={`nav-item ${page === 'system-status' ? 'active' : ''}`} onClick={() => setPage('system-status')}>
|
||||||
<span className="nav-item-icon">◉</span> System Status
|
<span className="nav-item-icon">◉</span> System Status
|
||||||
</button>
|
</button>
|
||||||
@@ -10786,6 +10877,7 @@
|
|||||||
{page === 'communications' && 'Communications'}
|
{page === 'communications' && 'Communications'}
|
||||||
{page === 'thesis' && 'Thesis'}
|
{page === 'thesis' && 'Thesis'}
|
||||||
{page === 'thesis-workshop' && 'Thesis Workshop'}
|
{page === 'thesis-workshop' && 'Thesis Workshop'}
|
||||||
|
{page === 'objections' && 'LP Objections'}
|
||||||
{page === 'system-status' && 'System Status'}
|
{page === 'system-status' && 'System Status'}
|
||||||
{page === 'email-capture' && 'Email Capture'}
|
{page === 'email-capture' && 'Email Capture'}
|
||||||
{page === 'feature-requests' && 'Feature Requests'}
|
{page === 'feature-requests' && 'Feature Requests'}
|
||||||
@@ -10818,6 +10910,7 @@
|
|||||||
{page === 'communications' && <CommunicationsPage token={token} onShowToast={showToast} />}
|
{page === 'communications' && <CommunicationsPage token={token} onShowToast={showToast} />}
|
||||||
{page === 'thesis' && <ThesisPage token={token} user={user} onShowToast={showToast} />}
|
{page === 'thesis' && <ThesisPage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'thesis-workshop' && <ThesisWorkshopPage token={token} user={user} onShowToast={showToast} />}
|
{page === 'thesis-workshop' && <ThesisWorkshopPage token={token} user={user} onShowToast={showToast} />}
|
||||||
|
{page === 'objections' && <ObjectionsPage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'system-status' && <SystemStatusPage token={token} user={user} onShowToast={showToast} />}
|
{page === 'system-status' && <SystemStatusPage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'email-capture' && <EmailCapturePage token={token} user={user} onShowToast={showToast} />}
|
{page === 'email-capture' && <EmailCapturePage token={token} user={user} onShowToast={showToast} />}
|
||||||
{page === 'feature-requests' && <FeatureRequestsPage token={token} onShowToast={showToast} user={user} />}
|
{page === 'feature-requests' && <FeatureRequestsPage token={token} onShowToast={showToast} user={user} />}
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:62 (fix backfill crash on no-Reply-To emails; Sync now retries errored mailboxes)
|
// * 0.1.0:62 (fix backfill crash on no-Reply-To emails; Sync now retries errored mailboxes)
|
||||||
// * 0.1.0:63 (System Status: storage usage — DB, attachments, backups, disk free)
|
// * 0.1.0:63 (System Status: storage usage — DB, attachments, backups, disk free)
|
||||||
// * 0.1.0:64 (email-activity agent: propose->review->approve grid notes; sync ~15 min)
|
// * 0.1.0:64 (email-activity agent: propose->review->approve grid notes; sync ~15 min)
|
||||||
// * Current: 0.1.0:65 (Email Capture: per-mailbox captured/matched counts)
|
// * 0.1.0:65 (Email Capture: per-mailbox captured/matched counts)
|
||||||
export const PACKAGE_VERSION = '0.1.0:65'
|
// * Current: 0.1.0:66 (LP Objections page: UI trigger for the Architect grounding pass)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:66'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ import { v_0_1_0_62 } from './v0.1.0.62'
|
|||||||
import { v_0_1_0_63 } from './v0.1.0.63'
|
import { v_0_1_0_63 } from './v0.1.0.63'
|
||||||
import { v_0_1_0_64 } from './v0.1.0.64'
|
import { v_0_1_0_64 } from './v0.1.0.64'
|
||||||
import { v_0_1_0_65 } from './v0.1.0.65'
|
import { v_0_1_0_65 } from './v0.1.0.65'
|
||||||
|
import { v_0_1_0_66 } from './v0.1.0.66'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_65,
|
current: v_0_1_0_66,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53, v_0_1_0_54, v_0_1_0_55, v_0_1_0_56, v_0_1_0_57, v_0_1_0_58, v_0_1_0_59, v_0_1_0_60, v_0_1_0_61, v_0_1_0_62, v_0_1_0_63, v_0_1_0_64, v_0_1_0_65],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// LP Objections page: a UI trigger for the Architect's grounding pass. An admin picks
|
||||||
|
// a segment (or All LPs) and runs grounding — the Architect mines matched LP emails and
|
||||||
|
// notes on the local model, removes every identifier through the redaction boundary,
|
||||||
|
// and asks Claude for the recurring objections + honest rebuttals. Only de-identified
|
||||||
|
// themes leave Ten31. Uses the existing /api/architect/ground route; no schema change.
|
||||||
|
export const v_0_1_0_66 = VersionInfo.of({
|
||||||
|
version: '0.1.0:66',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: [
|
||||||
|
'New LP Objections page: run the Architect grounding pass over your matched LP email',
|
||||||
|
'to surface the recurring objections (and the strongest honest rebuttals) for any',
|
||||||
|
'segment. Everything sensitive is de-identified on your own hardware before Claude sees',
|
||||||
|
'it — only objection themes leave Ten31. Results are a draft for your review.',
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user