Invoice Process: Execption UI v3#908
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Dependency License Review
License distribution
Excluded packages
|
| message.external_id && | ||
| inv.messages.some((m) => m.external_id === message.external_id) | ||
| ) { | ||
| state.invoices[invoiceId] = inv; |
| return false; | ||
| } | ||
| inv.messages.push(message); | ||
| state.invoices[invoiceId] = inv; |
|
|
||
| function resetInvoice(invoiceId) { | ||
| const state = readState(); | ||
| state.invoices[invoiceId] = blankInvoice(); |
| const LISTENER_PORT = process.env.LISTENER_PORT || "3010"; | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
| const LISTENER_PORT = process.env.LISTENER_PORT || "3010"; | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
| } = require("./escalation-card"); | ||
| const store = require("./store"); | ||
|
|
||
| const log = (...args) => console.log("[SLACK]", ...args); |
| const LISTENER_PORT = process.env.LISTENER_PORT || "3010"; | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
| const LISTENER_PORT = process.env.LISTENER_PORT || "3010"; | ||
|
|
||
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
📊 Coverage + size by packagePer-package bundle size on this PR (no JS/TS source changes detected under
"Coverage" is each package's own |
There was a problem hiding this comment.
Pull request overview
Adds an “Invoice Review” prototype flow to Apollo Vertex (v3 exception/timeline UI) and a bidirectional Slack demo integration, including local demo API routes and supporting runtime/state utilities.
Changes:
- Introduces a new invoice-review prototype template with a runtime store, timeline UI, decision dialogs, and fixture data.
- Adds a standalone Slack Socket Mode listener (isolated npm package) plus reset tooling, and Next.js API proxies to bridge the prototype ↔ listener.
- Extends the Vertex shell profile menu with an “extras” slot and adds supporting demo/dev scripts, tokens, and locale strings.
Reviewed changes
Copilot reviewed 32 out of 34 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds root-level demo scripts to run the Vertex app + Slack listener together. |
| apps/apollo-vertex/templates/invoice-review/README.md | Documents prototype status and lint-exclusion/migration guidance. |
| apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx | Supplier email draft/send modal used in the routing flow. |
| apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx | Renders AI suggested fix card and route actions (incl. reason dialog). |
| apps/apollo-vertex/templates/invoice-review/next/ReasonDialog.tsx | Shared confirm dialog with reason chips + optional note. |
| apps/apollo-vertex/templates/invoice-review/next/invoice-runtime.tsx | Adds per-invoice runtime store (events, waiting, disposition, highlights). |
| apps/apollo-vertex/templates/invoice-review/next/invoice-review-data.ts | Adds canonical types, fixtures, and seam/stub functions for the prototype. |
| apps/apollo-vertex/templates/invoice-review/next/HeaderDecision.tsx | Header disposition control (approve + hold/reject overflow using ReasonDialog). |
| apps/apollo-vertex/templates/invoice-review/next/ExceptionTimeline.tsx | Large timeline UI implementation with resolve choreography + waiting/terminal states. |
| apps/apollo-vertex/templates/invoice-review/invoice-version.tsx | Dev-only layout version switch (v1/v2/v3) persisted in localStorage. |
| apps/apollo-vertex/slack/store.js | JSON-file store for demo invoice state written by Slack listener. |
| apps/apollo-vertex/slack/server.js | Slack Bolt Socket Mode listener + local HTTP trigger/proxy endpoints. |
| apps/apollo-vertex/slack/reset-demo.js | Script to delete bot messages and reset demo store between rehearsals. |
| apps/apollo-vertex/slack/README.md | Setup/run docs for the Slack listener and demo flow. |
| apps/apollo-vertex/slack/package.json | Defines isolated npm package for Slack listener dependencies/scripts. |
| apps/apollo-vertex/slack/escalation-card.js | Block Kit escalation card builder posted into Slack. |
| apps/apollo-vertex/scripts/check-type-drift.mjs | Adds a font-size “type drift” guard for the new timeline UI. |
| apps/apollo-vertex/registry/shell/shell-user-profile-menu-items.tsx | Renders injected profile-menu extras via new hook/slot. |
| apps/apollo-vertex/registry/shell/shell-profile-extras.tsx | Adds provider/hook to inject extra items into the shell profile menu. |
| apps/apollo-vertex/registry/button/button.tsx | Updates button base classes (adds cursor-pointer). |
| apps/apollo-vertex/registry.json | Adds “insight-*” color tokens used by the new UI. |
| apps/apollo-vertex/package.json | Adds app-level demo scripts and extends lint with the type-drift check. |
| apps/apollo-vertex/locales/en.json | Adds new strings (“invoices”, “my_work”). |
| apps/apollo-vertex/lib/i18n.ts | Avoids re-initializing i18n; updates <html lang> when already initialized. |
| apps/apollo-vertex/app/invoice-review/page.tsx | Adds a route that renders the invoice review template full-screen. |
| apps/apollo-vertex/app/api/demo-trigger/route.ts | Proxies “trigger escalation” requests to the local Slack listener. |
| apps/apollo-vertex/app/api/demo-state/route.ts | Serves the Slack listener’s shared demo-state JSON to the prototype. |
| apps/apollo-vertex/app/api/demo-reply/route.ts | Proxies comms replies to Slack listener to post into Slack thread. |
| apps/apollo-vertex/app/_meta.ts | Hides the invoice-review route from navigation/meta. |
| apps/apollo-vertex/.oxlintrc.json | Excludes demo/prototype directories from oxlint. |
| apps/apollo-vertex/.gitignore | Ignores demo runtime state + isolated Slack listener artifacts. |
| apps/apollo-vertex/.env.example | Adds a template for Slack demo environment variables. |
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |
| "demo": "apps/apollo-vertex/slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm --filter apollo-vertex dev\" \"cd apps/apollo-vertex/slack && npm start\"", | ||
| "demo:reset": "cd apps/apollo-vertex/slack && npm run reset-demo", |
| "demo": "slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm dev\" \"cd slack && npm start\"", | ||
| "demo:reset": "cd slack && npm run reset-demo", |
| "@slack/bolt": "^4.4.0" | ||
| }, | ||
| "devDependencies": { | ||
| "concurrently": "^9.1.0" |
7ced88d to
05e4b5e
Compare
| function handleSendClick() { | ||
| // Reduced motion: commit immediately, no sending -> sent beats. | ||
| if (reducedMotion) { | ||
| onSend(buildDraft()); | ||
| return; | ||
| } | ||
| setSendPhase("sending"); | ||
| setTimeout(() => { | ||
| setSendPhase("sent"); | ||
| setTimeout(() => onSend(buildDraft()), 500); | ||
| }, 600); | ||
| } |
| <button | ||
| type="button" | ||
| className="cursor-pointer hover:opacity-90" | ||
| > | ||
| {action} | ||
| </button> |
| res.setHeader("Access-Control-Allow-Origin", "*"); | ||
| res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); | ||
| res.setHeader("Access-Control-Allow-Headers", "Content-Type"); |
| "demo": "apps/apollo-vertex/slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm --filter apollo-vertex dev\" \"cd apps/apollo-vertex/slack && npm start\"", | ||
| "demo:reset": "cd apps/apollo-vertex/slack && npm run reset-demo", |
| // it (chat.update) after an action is taken. | ||
| let lastMessage = null; | ||
|
|
||
| const required = ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"]; |
05e4b5e to
5535b9f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 34 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
package.json:24
- The root
demoscript hardcodes a nestednode_modules/.bin/concurrentlypath, which is brittle (breaks if deps aren’t installed yet and is not portable across environments). Sinceapollo-vertexalready definesdemo/demo:reset, prefer delegating to that package’s scripts so there’s a single source of truth.
| <Button variant="ghost" size="sm" onClick={() => setOpen(false)}> | ||
| Cancel | ||
| </Button> | ||
| <Button variant={commitVariant} size="sm" onClick={commit}> | ||
| {commitLabel} | ||
| </Button> |
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| disabled={disabled} | ||
| onClick={() => setRouteDialogOpen(true)} | ||
| onMouseEnter={handleAim} | ||
| onFocus={handleAim} | ||
| onMouseLeave={handleClearAim} | ||
| onBlur={handleClearAim} | ||
| > |
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| disabled={disabled} | ||
| onClick={() => onResolve(suggestion)} | ||
| onMouseEnter={handleAim} | ||
| onFocus={handleAim} | ||
| onMouseLeave={handleClearAim} | ||
| onBlur={handleClearAim} | ||
| > |
| "use client"; | ||
|
|
||
| import { Check, Loader2, Send } from "lucide-react"; | ||
| import { useId, useRef, useState } from "react"; |
| const bodyRef = useRef<HTMLTextAreaElement>(null); | ||
| const markGradientId = useId(); | ||
| const busy = sendPhase !== "idle"; | ||
| // The body is focused programmatically on open; text fields still match | ||
| // :focus-visible on programmatic focus, so gate the ring on a real interaction | ||
| // (keydown / pointer) to keep the open state ring-free with just its border. | ||
| const [ringArmed, setRingArmed] = useState(false); | ||
| const [bodyEdited, setBodyEdited] = useState(false); | ||
|
|
||
| function buildDraft(): SupplierEmailDraft { | ||
| return { to, cc: cc.trim() || undefined, subject, body }; | ||
| } | ||
|
|
||
| function handleSendClick() { | ||
| // Reduced motion: commit immediately, no sending -> sent beats. | ||
| if (reducedMotion) { | ||
| onSend(buildDraft()); | ||
| return; | ||
| } | ||
| setSendPhase("sending"); | ||
| setTimeout(() => { | ||
| setSendPhase("sent"); | ||
| setTimeout(() => onSend(buildDraft()), 500); | ||
| }, 600); | ||
| } |
| server.listen(LISTENER_PORT, () => | ||
| log( | ||
| `HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`, | ||
| ), | ||
| ); |
5535b9f to
df828b4
Compare
df828b4 to
8921124
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 34 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (2)
package.json:24
- The root-level demo scripts hardcode the Slack listener’s local node_modules path (apps/apollo-vertex/slack/node_modules/.bin/concurrently). That makes
pnpm demobrittle and duplicates the same logic already added to apps/apollo-vertex. Prefer delegating to the workspace script so the root stays stable even if the Slack folder changes.
apps/apollo-vertex/slack/server.js:369 - The local HTTP trigger server binds without an explicit host, which typically listens on all interfaces. Combined with permissive CORS (
Access-Control-Allow-Origin: *), this makes it easier for other machines on the network (or a malicious webpage) to hit the trigger endpoints during a demo. Bind explicitly to 127.0.0.1 to keep it local-only.
server.listen(LISTENER_PORT, () =>
log(
`HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
),
);
| const buttonVariants = cva( | ||
| "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||
| "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", | ||
| { |
| const [cursorMap, setCursorMap] = useState<Record<string, string>>({}); | ||
|
|
||
| const value = useMemo<RuntimeStore>( | ||
| () => ({ | ||
| getRuntime: (id) => map[id] ?? EMPTY, | ||
| getCursor: (id) => cursorMap[id], | ||
| setCursor: (id, exceptionId) => | ||
| setCursorMap((prev) => { | ||
| const next = exceptionId ?? ""; | ||
| if (prev[id] === next) return prev; | ||
| return { ...prev, [id]: next }; | ||
| }), |
| () => | ||
| resolve({ | ||
| id: `${invoiceId}-${exceptionId}-msg`, | ||
| sentTime: "just now", |
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
| export async function POST(request: NextRequest) { | ||
| let body = "{}"; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 34 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
package.json:24
- The root-level demo scripts hardcode the Slack listener’s local node_modules bin path and duplicate the same orchestration that now exists in apps/apollo-vertex/package.json. This is brittle (breaks if slack deps aren’t installed yet, and is less portable across shells/OSes) and creates two sources of truth for the demo command.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:84 - handleSendClick schedules nested setTimeout callbacks without any cleanup. If the modal unmounts (navigation, parent state reset, React strict-mode re-mounting in dev), these timeouts can fire after unmount and call setSendPhase/onSend unexpectedly. Consider storing timeout IDs in a ref and clearing them in a cleanup effect (and/or before scheduling a new send).
| try { | ||
| const res = await fetch( | ||
| `http://localhost:${LISTENER_PORT}/trigger-escalation`, | ||
| { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body, | ||
| }, | ||
| ); | ||
| const data = await res.json(); | ||
| return NextResponse.json(data, { status: res.ok ? 200 : 502 }); | ||
| } catch { |
| try { | ||
| const res = await fetch(`http://localhost:${LISTENER_PORT}/reply`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body, | ||
| }); | ||
| const data = await res.json(); | ||
| return NextResponse.json(data, { status: res.ok ? 200 : res.status }); | ||
| } catch { |
| useEffect(() => { | ||
| const saved = localStorage.getItem(STORAGE_KEY); | ||
| const migrated = | ||
| saved === "current" ? "v1" : saved === "next" ? "v2" : saved; | ||
| if (isVersion(migrated)) setVersionState(migrated); | ||
| }, []); | ||
|
|
||
| const setVersion = (v: InvoiceVersion) => { | ||
| setVersionState(v); | ||
| localStorage.setItem(STORAGE_KEY, v); | ||
| }; |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 34 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
package.json:24
- The
demo/demo:resetscripts hardcodeapps/apollo-vertex/slack/node_modules/.bin/concurrently, which will fail unless the Slack listener dependencies have been installed and the path layout matches exactly. Prefer invoking a pinnedconcurrentlyvia pnpm so the script works from a clean checkout without relying on another subproject’snode_modules.
apps/apollo-vertex/package.json:10 - This
demoscript depends onslack/node_modules/.bin/concurrently, which is brittle and breaks ifapps/apollo-vertex/slackhasn’t beennpm install’d yet. Using a pinned pnpm dlx (or addingconcurrentlyto this package’s devDependencies and usingpnpm exec concurrently) makes the command runnable from a clean checkout.
"demo": "slack/node_modules/.bin/concurrently --names APP,SLACK --prefix-colors cyan,magenta \"pnpm dev\" \"cd slack && npm start\"",
"demo:reset": "cd slack && npm run reset-demo",
| "use client"; | ||
| import { InvoiceReviewTemplate } from "@/templates/invoice-review/InvoiceReviewTemplate"; |
| <input | ||
| value={to} | ||
| onChange={(e) => setTo(e.target.value)} | ||
| className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring" | ||
| /> |
| <input | ||
| value={cc} | ||
| onChange={(e) => setCc(e.target.value)} | ||
| placeholder="Add CC…" | ||
| className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs placeholder:text-muted-foreground/40 focus:outline-none focus:ring-1 focus:ring-ring" | ||
| /> |
| <input | ||
| value={subject} | ||
| onChange={(e) => setSubject(e.target.value)} | ||
| className="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-ring" | ||
| /> |
| <textarea | ||
| ref={bodyRef} | ||
| value={body} | ||
| onChange={(e) => { | ||
| setBody(e.target.value); |
| <Button | ||
| key={c} | ||
| type="button" | ||
| variant={reason === c ? "default" : "outline"} | ||
| size="sm" | ||
| onClick={() => setReason(c)} | ||
| > |
Add a full correction pipeline so fix actions and manual edits converge on a single runtime source of truth, and auto-resolve exceptions when the predicate registry clears them. Section A - correction pipeline foundation: - Add LineCorrection, DetailCorrections, PredicateBaseData types - Add correction field to Suggestion; suggestionLabel derives copy from it so qty strings never hardcode what the record already knows - Add detailCorrections to InvoiceRuntime; commitResolve merges patches - Add RESOLUTION_PREDICATES registry with qty-over-invoiced + vat-mismatch - Add correctDetail store mutation: merges corrections, runs predicates, emits auto-resolve + revalidated events atomically - Fix INV-30291 seed to use PO quantities (10/25/20) as single source of truth; correction payloads derive label copy from those values - ExceptionTimeline threads detailCorrections through resolveActive/commit - showInSource calls onRequestEdit for registered exception types (vat) Section B - edit mode: - EditModeContext provides editMode, editFocusField, enter/exitEditMode - CorrectedMark inline UserPen icon shown next to corrected field labels - DetailsCombinedTab merges rt.detailCorrections over base data (cd/dc); uses correctedLines for line items table; Edit button on heading - DetailsEditForm: controlled inputs for all extracted fields + lines, dirty tracking with label suffix and border highlight, field count footer - EditModeView: absolute overlay covering center+right, mode header bar with Cancel and Save & re-check, 420px form + flex-1 source split, Esc-to-cancel with AlertDialog discard guard - CenterPanelNext accepts onRequestEdit and passes it to ExceptionTimeline - InvoiceDetailPane provides EditModeContext and renders EditModeView when editMode is true Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…phy, field sync Section A: Route all fix actions through correctDetail (no direct resolvedIds). - outside-po-period predicate + billing-account predicate - INV-55832 date fix via correction.documentDateFormatted - INV-66216 VAT verify via correction.vat - INV-60118 account via correction.billingAccount - correctDetail extras: surfaced/dataPatch/settledSub on same mutation - SessionStorage persistence: corrections survive page reload Section B: Edit mode entrance choreography. - Overlay fades 120ms; form slides translateX 260ms ease-out - PDF column fades + rises 8px with 100ms delay - Exit reverses; doExit drives cancel/save/discard/Esc paths - Loading skeleton on PDF column until pdfReady - prefers-reduced-motion cuts all transitions Section C: Field source sync. - EDIT_FIELD_ANCHORS map + fieldAnchor() + fp() onFocus/onBlur - setFieldHighlight in runtime; takes priority over exception highlight - SourceTab: fieldHighlight overrides exceptionHighlight in activeAnchors - data-anchor on vendor, vendorEmail, billTo, billAddress Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…settle pulses
A. INV-30291 race fix: parseInt instead of Number() for PO qty string ("10 units"
was parsing as NaN, predicate never cleared). nextOpenId now prefers exceptions
after the current index before wrapping, so the cursor advances forward instead
of jumping back to the first item in the list.
B. Marker taxonomy: corrected events now render person marker (reviewer), not
sparkle. Auto-resolved events (auto: true) render sparkle (agent) instead of
the UserRoundCheck resolved marker.
C. correctionPulse added to InvoiceRuntime; correctDetail computes it atomically
(detailFields, lineNums, autoResolvedIds). DetailsCombinedTab settle-pulses
corrected scalar fields and qty cells. FindingsMap rows settle-pulse when their
ID is in autoResolvedIds. Edit-mode save defers correctDetail until 200ms after
exit so the pulse fires on the visible panel, not the hidden overlay.
D. DetailsEditForm section labels: remove uppercase/tracking-wide Tailwind classes
so "Invoice details", "Parties", "Line items" render in sentence-case weight.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds resetAllRuntimes() to the runtime store — wipes in-memory map, cursor map, and sessionStorage in one call. Surfaces as a small RotateCcw icon button next to "My Work" in the LeftNav header, with a tooltip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hovering or focusing a data-mutating fix action (suggest_correction, verify) shows a 1.5px dashed warning-toned outline on the evidence value in the exception card and on the matching field/line in the Details panel. Non- mutating actions (route, wait) produce no aim. verify aims at evidence only (empty correction = no panel fields targeted). Aim clears instantly on mouseleave/blur. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…do toast
A. Unifies the evidence-value affordance: replaces the Highlighter with a Pencil
on every warn-toned FindingCell, removes the sourceAnchors guard so the pen
always shows, and expands EXCEPTION_EDIT_FIELD with outside-po-period,
new-vendor, and billing-account mappings. onRequestEdit now accepts an
optional field key so it opens edit mode even when no direct mapping exists.
B. Ghost preview on aim: while a mutating fix action is hovered/focused, the
warn evidence cell shows "current → new" inside the dashed ring and the
corresponding panel metadata field echoes the same inline preview. Line-item
cells stay ring-only. aimGhostValue is computed from the first scalar entry
in rt.aimCorrection and threaded through ExceptionGroup → Stage →
LiveExceptionContent → FindingView → FindingCell.
C. Undo via toast: after any correction commit a Sonner toast shows "Corrected
{Field}" or "{N} fields corrected" with an 8s Undo action. Undo calls
revertDetail which restores detailCorrections to the pre-correction snapshot,
un-resolves the auto-cleared exceptions, and appends a combined
"Correction reverted" person event + "Re-validated" sparkle event. Previous
toast is dismissed before the new one appears.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts, Bill to spacing fix Replace FindingsMap + ExceptionIndex with IssueDots pagination dots and carousel prev/next arrow buttons on the "Needs your decision" header row. One dot per non-waiting issue; pending = hollow circle, current = amber pill, resolved = muted fill with settle pulse on auto-resolve. Arrow buttons are 26px, hairline border, disabled-at-ends; keyboard ←/→ works when focus is in the decision header. Single issue hides arrows and dots. Removes the exceptionListVariant prop entirely. Also adds mt-0.5 to the Bill to address block so its spacing from the company name matches the Vendor section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each edit-form input whose field participates in an open finding renders a
reference line beneath it: counterpart label, value, provenance, and a
"Use this value" action that fills the input and marks it sourced.
On save, sourced corrections get per-field events noting the origin
("applied vendor master value", "applied PO quantity") instead of the
plain old→new copy. Manually typed corrections keep the old→new format.
Annotations live-derive from the open exception set, so resolved findings
drop their reference rows and undo restores them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nce seam Adds an explicit `reference` seam to InvoiceException: each exception that implicates a specific editable field now carries the system-of-record value the reviewer should correct toward. Seeded on: INV-GRN-001 price mismatch (PO amount), INV-66216 VAT mismatch (vendor master), INV-55832 outside PO period (PO window end date), and INV-30291/INV-30292 qty-over-invoiced exceptions (PO qty per line). The edit form derives annotations from live open exceptions via referenceFieldKeyToFormKey. Each annotated input shows the reference value beneath it; Use this value fills the field. On save, corrections whose final value matches the reference produce applied-source event copy rather than the plain old→new format. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds optional shortDescription to the line item type; renders below the item title as text-xs muted-foreground, truncated to one line with a tooltip on hover exposing the full string. Seeded on INV-GRN-001 and INV-66216 for visibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace annotation line with a compact chip (<value> · <source>) that acts
as the apply action directly. Dry-run runPredicates in handleSave to detect
still-failing exceptions and emit targeted re-check copy ("VAT number still
differs from vendor master") instead of the generic "nothing new". Fix
FIELD_LABELS casing so event labels read "Corrected VAT number" and
"Corrected document date" correctly in-sentence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…revert - Correction toast uses inverted (dark bg / light text) style via classNames - "High value" subline replaced with Badge secondary/warning for a11y - IssueDots reverted to uniform 7px bg-foreground dots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n issues Navigation domain is now open issues only. Dots, arrows, and the jump menu all traverse open issues; Issue n of m counts position among open, not total. Resolved issues settle-then-disappear from dots; the jump menu drops Done rows entirely. Counter and chrome hide when only one open issue remains. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…suggestions Mark verified and any future attest-shaped suggestions now show the dashed aim ring on the attested field without a ghost arrow or incoming value. The ring means "this value is what the action concerns" — not "this will change." Empty-string sentinels in suggestionAimCorrection let isAimed fire (value !== undefined) while aimGhost returns "" (falsy), suppressing the → X arrow. resolveActive also guards detailCorrections from verify type so committing an attestation never calls correctDetail — no field is written, no CorrectedMark appears on the panel value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All verify/attest suggestions now say "Keep X" or "Confirm Y" instead of the generic "Mark verified"/"Mark reviewed". INV-66216 VAT card gets three distinct actions (Correct, Keep invoice value, Route). Per-suggestion resolution overrides let each fix path emit its own event copy. suggestionLabel gains a verify fallback and VAT auto-derivation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix cards carry at most two buttons (direct fix + Send to owner) and a ✕ icon at the header-right for the keep-as-is attestation path. ✕ hover fires the aim ring (empty sentinels); click commits through the store and shows an undo toast. Route labels renamed to "Send to owner" with a tooltip disclosing the full recipient. INV-55832 gains a verify suggestion for the ✕ slot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…is visible When hovering a mutation fix and its incoming value is already shown in the evidence pair's reference column (e.g. "On file: US-82-4470911"), suppress the inline → ghost on the target cell and switch to pair-mode: solid warning ring on the source (reference) cell, existing dashed ring on the warn (target) cell, and a small ← arrow at the divider pointing source→target. Mouseleave clears all three atomically. Ghost previews in the Details panel and edit form are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Center-context hover (fix action buttons, ✕) shows aim rings and ghosts only in the evidence pair chips inside ExceptionTimeline. The Details panel no longer reacts to rt.aimCorrection during hover — it remains completely still. Commit pulses (correctionPulse nonce) are unchanged, as are edit-form aim chips which use their own local context. Batch-line aim follows the same rule: panel cells ring only after commit, never during hover. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tions When a fix-action (agent-sourced) commits, the corrected panel value briefly glows with an inset box-shadow built from --ai-glow-start / --ai-glow-end tokens, then settles to clean text. Manually typed corrections settle with neutral amber peak → transparent rest. Removes the lingering amber box that was persisting on corrected values — amber is reserved for flagged state, not fixed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three-beat sequence replaces the landing glow for agent-sourced fixes: fix button shows Applying… spinner (disabled) while the Details panel shows an AI-gradient ring + anchored pill; at ~500ms correctDetail commits (value swaps, undo toast + events fire); ring transitions to emerald success and pill reads Updated; at ~1.6s both fade to clean. Manual/typed corrections retain the neutral amber settle unchanged. Reduced motion skips the arc entirely (instant rest state). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split correctDetail into step1 (ACT) / step2 (REST) so the center timeline, queue chip, and carousel stay frozen while the panel pill is visible. The correction and one corrected event land at ACT; auto-resolve IDs, auto-clear events, the re-validated event, and the carousel advance all fire at REST via correctDetailStep2. Adds rt.arcState guard to resolveActive to prevent concurrent arcs during the 1800ms sequence. Range-aim containment: replaces the equality-only sourceCell check in FindingView with c.value.includes(aimGhostValue), so date windows and numeric ranges trigger pair-mode (ring on counterpart, arrow at divider, no ghost) when the incoming value appears anywhere in the counterpart string. Covers INV-55832 outside-po-period where Apr 30, 2026 is the end of Jan 1 - Apr 30, 2026. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…em corrections Timeline typography: canonical ChildRow component (13/500 label, 12/400 desc+ts) shared by AgentHistoryPeek and CollapsedClusterRow; SubEventRow delegates to it. Leading chevrons on both peek systems; ml-4/py-2 fold spacing. Arc phase machine: glow overlay opacity now driven by arcState.phase (not a 1700ms CSS keyframe with its own clock). CLAIM fades glow in over 450ms ease-out, ACT+CONFIRM hold at 0.15, RELEASE fades out over 300ms ease-in. Timers aligned: ACT→500ms, CONFIRM→750ms, RELEASE→1350ms, REST→1650ms. Line-item corrections: price-mismatch predicate added to RESOLUTION_PREDICATES. exc-price (GRN-001) and exc-30292-qoi-1/3 gain correction payloads (amount+qty). INV-30291 corrections include corrected line amounts. DetailsCombinedTab and TopBarNext derive correctedLinesTotal/correctedAmount from correctedLines so panel total and top-bar Amount cascade when line amounts are corrected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire TopBarNext: doApprove routes through runtime.approveGated so the store blocks approval even if the disabled prop is bypassed via devtools. Adds doWait/doSendBack, threads openCount/waitingCount into HeaderDecision, and inserts waitingDisp into the effectiveStatus chain before onHoldDisp. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-step feedback flow on the fix card footer: positive path records sentiment and closes; correction path collects chips + note, writes a person event immediately and a sparkle event after a 2s regen interval, then shows seeded no-change prose. revisionCount derived from correction records; superseded prose appended to audit trail only. Primary-filled split button on the PO action, footer legal line with info icon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
78e3e0d to
e254a61
Compare
|
Closing in favour of a fresh draft PR. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 35 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (9)
package.json:24
- The root demo scripts hardcode the Slack listener's local node_modules/.bin path, which will fail unless the Slack subfolder has been npm-installed and also duplicates the package-level scripts. Prefer delegating to the apollo-vertex package scripts so the root stays stable if paths change.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:4 - This component starts send-animation timers; adding a cleanup effect requires importing useEffect from React to avoid setState calls after unmount.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:67 - handleSendClick uses nested setTimeout calls without cleanup. If the modal unmounts (e.g., parent conditionally renders it), these callbacks can fire after unmount and attempt state updates. Store the timer IDs in refs and clear them in a cleanup effect.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:146 - The "To" input is visually labeled but has no programmatic label, so screen readers will announce it as an unlabeled textbox. Add an aria-label (or associate a ) and set an appropriate input type.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:167 - The "CC" input is visually labeled but lacks a programmatic label, which harms screen reader usability. Add an aria-label (or associate a ) and set an appropriate input type.
apps/apollo-vertex/templates/invoice-review/next/SupplierEmailModal.tsx:178 - The "Subject" input is visually labeled but has no programmatic label. Add an aria-label (or associate a ) so assistive tech announces it correctly.
apps/apollo-vertex/templates/invoice-review/next/SuggestedFixCard.tsx:109 - regenTimerRef is cleared before starting a new timer, but it's not cleared on unmount. Add a cleanup effect so the pending timeout can't fire after the card is removed.
apps/apollo-vertex/slack/server.js:304 - This local HTTP listener sets permissive CORS headers, but the prototype reaches it server-to-server via Next.js proxy routes. Removing these headers reduces the chance of unintended cross-origin browser access when the listener is running.
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
apps/apollo-vertex/slack/server.js:369
- server.listen() currently binds on all interfaces by default, which can expose the demo endpoints (/trigger-escalation and /reply) to the local network and allow posting into Slack if someone can reach the port. Bind explicitly to 127.0.0.1 for local-only access.
server.listen(LISTENER_PORT, () =>
log(
`HTTP trigger on http://localhost:${LISTENER_PORT} (POST /trigger-escalation)`,
),
);
| Pencil, | ||
| X, | ||
| } from "lucide-react"; | ||
| import { useRef, useState } from "react"; |
No description provided.