lead: one entry per submission, and the finding above the fold
Two things on the lead page. THE AUDIT TRAIL REPEATED ITSELF. "Qualify Lead" appeared three times in a row and "Partner Agent Lead" twice, which reads as the AI having done the same thing three times. It had not. One submission writes several rows, told apart by execution_state: zk-act-qualify TRIGGER_PERFORMED the trigger's commit zk-act-qualify TRIGGER_PERFORMED again, on the settle path zk-act-qualify zk-state-qualified the one that moved the lead Only the last means anything to somebody reading the file. They are now collapsed into one entry carrying the earliest timestamp — when the operator acted — and whichever state and payload is populated. Matched on (same activity, within fifteen seconds) rather than on execution_state, so a genuine repeat survives: Collect Documents really is performed twice on a lead whose first upload was short, and those are minutes apart. And matched by looking BACK through recent entries rather than at the previous one, because the platform interleaves a DATA_UPDATE row between the two halves of a submission — the rows to merge are near each other in time but not adjacent in the list. THE AI PARAGRAPHS HAD NO SUMMARY. attribution_reason and eligibility_reason run to a paragraph each and opened folded, so the finding could not be read without expanding. ClampText now lifts the first sentence out as a headline and puts the rest behind "Show the reasoning" — no new field, and nothing invented: the employees already write a conclusion and then its evidence. It falls back to plain folding when the split would be useless — no sentence terminator, a first sentence that is the whole paragraph, or one short enough to be a fragment rather than a finding. The other half of that is in the charter (70_verdict_first.sql). The headline is only as good as the sentence, and Intake was opening with "Tool 29601 confirmed..." — an internal id — because it had been told to "name the evidence, not the conclusion". Right about content, wrong about order: it now leads with what it decided and gives the evidence second.
This commit is contained in:
parent
4e5df66389
commit
2d9c072572
@ -57,3 +57,27 @@
|
||||
.clamp.is-open .clamp__more svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* The headline lifted out of an AI paragraph. Slightly heavier than the body
|
||||
and never clamped — it is one sentence by construction, and the point of it
|
||||
is that the finding can be read without opening anything. */
|
||||
.clamp__lead {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--zk-ink);
|
||||
font-weight: 500;
|
||||
}
|
||||
.clamp__lead + .clamp__text { margin-top: 8px; }
|
||||
|
||||
/* The finding, lifted out of an AI paragraph. Heavier than the body and never
|
||||
clamped — it is one sentence by construction, and the whole point is that it
|
||||
can be read without opening anything. */
|
||||
.clamp__lead {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
color: var(--zk-ink);
|
||||
}
|
||||
.clamp__lead + .clamp__text { margin-top: 8px; }
|
||||
|
||||
@ -1,15 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import './ClampText.css'
|
||||
|
||||
/**
|
||||
* Splits a block of AI prose into a finding and its working.
|
||||
*
|
||||
* The employees write the conclusion first and the evidence after it —
|
||||
* "POSP-77341 is active and empanelled for motor." then four sentences of
|
||||
* registry detail. So the first sentence already IS the summary, and lifting it
|
||||
* out gives one without inventing text or asking the model for a second field
|
||||
* it would have to be trusted to keep in step with the first.
|
||||
*
|
||||
* Returns null when the split would be useless: no sentence terminator, a first
|
||||
* sentence so long it is the paragraph again, or one so short it is a fragment
|
||||
* rather than a finding. Those fall back to plain folding.
|
||||
*/
|
||||
function splitLead(value) {
|
||||
const m = value.match(/^(.{40,220}?[.!?])(\s+)(\S[\s\S]*)$/)
|
||||
if (!m) return null
|
||||
return { lead: m[1].trim(), rest: m[3].trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prose, folded.
|
||||
*
|
||||
* The AI employees write at length — a recommendation rationale runs past 1,500
|
||||
* characters — and a lead file that prints all of it is a document, not a
|
||||
* screen. Anything long opens as a few lines with the rest one click away, so
|
||||
* the page can be scanned and still holds everything for whoever wants it.
|
||||
* screen. Anything long shows its finding, with the reasoning one click away.
|
||||
*
|
||||
* Whether to fold is decided on length, not on measuring the rendered box: a
|
||||
* Whether to fold is decided on length, not by measuring the rendered box: a
|
||||
* ref-and-measure pass would reflow on every resize to answer a question the
|
||||
* string itself already answers.
|
||||
*/
|
||||
@ -19,6 +37,10 @@ export default function ClampText({ text, lines = 3, threshold = 150 }) {
|
||||
|
||||
if (value.length <= threshold) return <p className="clamp__text clamp__text--short">{value}</p>
|
||||
|
||||
const split = splitLead(value)
|
||||
|
||||
// No sensible headline — fold the whole thing, as before.
|
||||
if (!split) {
|
||||
return (
|
||||
<div className={'clamp' + (open ? ' is-open' : '')}>
|
||||
<p className="clamp__text" style={{ '--clamp-lines': lines }}>{value}</p>
|
||||
@ -32,3 +54,18 @@ export default function ClampText({ text, lines = 3, threshold = 150 }) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'clamp' + (open ? ' is-open' : '')}>
|
||||
<p className="clamp__lead">{split.lead}</p>
|
||||
{open ? <p className="clamp__text clamp__text--short">{split.rest}</p> : null}
|
||||
<button type="button" className="clamp__more" onClick={() => setOpen((o) => !o)}>
|
||||
{open ? 'Hide the reasoning' : 'Show the reasoning'}
|
||||
<svg viewBox="0 0 12 12" aria-hidden="true">
|
||||
<path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" strokeWidth="1.5"
|
||||
strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -84,7 +84,59 @@ export default function Timeline({ instanceId }) {
|
||||
// Oldest first: a timeline reads forwards.
|
||||
const ordered = [...rows].sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))
|
||||
|
||||
const items = ordered.map((r, i) => {
|
||||
// ONE SUBMISSION WRITES SEVERAL ROWS. The platform records each stage of a
|
||||
// submission separately, told apart by execution_state:
|
||||
//
|
||||
// zk-act-qualify TRIGGER_PERFORMED the trigger's commit
|
||||
// zk-act-qualify TRIGGER_PERFORMED ...again, on the settle path
|
||||
// zk-act-qualify zk-state-qualified the one that moved the lead
|
||||
//
|
||||
// Rendered literally that is "Qualify Lead" three times in a row, which
|
||||
// reads as the AI having done the same thing three times. Only the row
|
||||
// carrying a real state means anything to somebody reading the file.
|
||||
//
|
||||
// Collapsed on (same activity, within fifteen seconds) rather than on the
|
||||
// execution_state alone, because a genuine repeat has to survive: Collect
|
||||
// Documents really is performed twice on a lead whose first upload was
|
||||
// incomplete, and those are minutes apart, not milliseconds.
|
||||
//
|
||||
// The kept row takes the EARLIEST timestamp — when the operator acted — and
|
||||
// whichever state and payload is actually populated.
|
||||
const isStage = (v) => STAGES.some((s) => s.uid === v)
|
||||
const SAME_SUBMISSION_MS = 15000
|
||||
|
||||
const merged = []
|
||||
for (const r of ordered) {
|
||||
// Look BACK for a match rather than only at the previous entry: the
|
||||
// platform interleaves a DATA_UPDATE row between the two halves of one
|
||||
// submission, so the rows to merge are near each other in time but not
|
||||
// adjacent in the list.
|
||||
let at = -1
|
||||
for (let i = merged.length - 1; i >= 0; i--) {
|
||||
if (Date.parse(r.created_at) - Date.parse(merged[i].created_at) >= SAME_SUBMISSION_MS) break
|
||||
if (merged[i].activity_id === r.activity_id) { at = i; break }
|
||||
}
|
||||
|
||||
if (at >= 0) {
|
||||
const prev = merged[at]
|
||||
merged[at] = {
|
||||
...prev,
|
||||
// The settle row is the one that names the resulting stage.
|
||||
execution_state: isStage(r.execution_state) ? r.execution_state : prev.execution_state,
|
||||
// Whichever row actually carries the submission and the AI's working.
|
||||
data: (r.data && Object.keys(r.data).length) ? r.data : prev.data,
|
||||
ai_reasoning: r.ai_reasoning || prev.ai_reasoning,
|
||||
ai_confidence: r.ai_confidence ?? prev.ai_confidence,
|
||||
user_name: prev.user_name || r.user_name,
|
||||
user_roles: (prev.user_roles && prev.user_roles.length) ? prev.user_roles : r.user_roles,
|
||||
created_at: prev.created_at,
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push(r)
|
||||
}
|
||||
|
||||
const items = merged.map((r, i) => {
|
||||
const roles = r.user_roles || []
|
||||
const aiRole = roles.find((x) => AI_ROLES[x])
|
||||
const isSystem = r.activity_id === 'DATA_UPDATE'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user