feat(pilot): Phase 3 — Suggested fix tracking + Resolve preview with state_version cache
Adds the AI-proposed resolution path and the inline preview of the
markdown that will be posted to the customer ticket on Resolve. The
preview is keyed on (session_id, ai_sessions.state_version) so back-to-
back fetches against unchanged state hit an in-process cache instead
of paying for a Sonnet call.
Backend:
- preview_cache: in-process LRU keyed on (kind, session_id, state_version).
No TTL — state_version is the source of truth. Soft-cap 5000 entries.
- unified_chat_service: [SUGGEST_FIX] parser (last-block-wins, JSON
payload, confidence clamped 0-100), supersession persistence (sets
superseded_at on prior active row), atomic state_version bump.
- ResolutionNoteGeneratorService: pulls session, facts, active fix, and
redacted script_generations into a structured input bundle for Sonnet;
produces the four-section markdown (Problem / What we confirmed /
Root cause / Resolution). Sensitive script parameters redacted via
ScriptTemplateEngine.redact_sensitive driven by the template's
parameters_schema.
- /api/v1/ai-sessions/{id}/suggested-fixes/active — 200 with the active
fix or 404.
- /api/v1/ai-sessions/{id}/suggested-fixes/{fix_id}/decision — records
one_off / draft_template / build_template / dismissed; dismiss
supersedes; bumps state_version. 409 on dismissing an already-
superseded fix.
- /api/v1/ai-sessions/{id}/resolution-note/preview — generates or returns
cached markdown; from_cache flag in payload signals cache hit.
- scripts.py POST /generate now bumps state_version on the linked
ai_session_id when present (third source of preview-cache invalidation
per Section 5.5).
- ASSISTANT_SYSTEM_PROMPT documents [SUGGEST_FIX] (when to/not to emit,
format, supersession semantics).
- 12 tests covering the parser (well-formed, last-wins, malformed,
confidence clamping), supersession + state_version invariant, all
decision branches, preview cache hit-on-no-change + miss-after-write.
Frontend:
- src/components/pilot/sections/SuggestedFix.tsx — amber-accented card
with confidence badge; dismiss action wired to the decision endpoint.
- src/components/pilot/ResolutionNotePreview.tsx — popover with refresh,
loading state, cached/fresh indicator, ticket-ref display.
- src/api/sessionSuggestedFixes.ts — typed client; getActive normalizes
404 to null so callers don't have to special-case.
- TaskLane gains suggestedFixSlot + bottomSlot props (rendered after
Diagnostic Checks; bottomSlot anchors the Resolve action).
- AssistantChatPage: refreshSessionDerived helper batches fact + fix
refresh; fact mutations and chat sends both schedule a 500ms-debounced
preview refresh per the Section 5.5 spec.
Verified end-to-end against the dev stack with a real Sonnet call:
- /active 404 → fact create → preview generates four-section markdown
grounded only in provided facts → second preview call hits cache
(from_cache=true, no LLM call) → fact write 2 → cache miss, regenerates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,12 @@ interface TaskLaneProps {
|
||||
// shape lets the parent own fact-fetching and state-version polling without
|
||||
// pulling that concern into TaskLane.
|
||||
whatWeKnowSlot?: React.ReactNode
|
||||
// Phase 3: Suggested fix card, rendered below Diagnostic Checks.
|
||||
suggestedFixSlot?: React.ReactNode
|
||||
// Phase 3: bottom-of-lane slot for the Resolve action bar + preview popover
|
||||
// (parent owns state). Renders inside the scrollable body so the popover
|
||||
// stays anchored as the lane scrolls.
|
||||
bottomSlot?: React.ReactNode
|
||||
}
|
||||
|
||||
// ── Storage helpers ──
|
||||
@@ -69,7 +75,7 @@ export function clearTaskState(sessionId: string) {
|
||||
|
||||
// ── Component ──
|
||||
|
||||
export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading, whatWeKnowSlot }: TaskLaneProps) {
|
||||
export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading, whatWeKnowSlot, suggestedFixSlot, bottomSlot }: TaskLaneProps) {
|
||||
const [tasks, setTasks] = useState<TaskResponse[]>(() => {
|
||||
// Try to restore saved state for this session (preserves user's in-progress answers)
|
||||
if (sessionId) {
|
||||
@@ -503,6 +509,12 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ── Suggested fix (Phase 3) ── */}
|
||||
{suggestedFixSlot}
|
||||
|
||||
{/* ── Resolve action bar + preview popover (Phase 3) ── */}
|
||||
{bottomSlot}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
107
frontend/src/components/pilot/ResolutionNotePreview.tsx
Normal file
107
frontend/src/components/pilot/ResolutionNotePreview.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* ResolutionNotePreview — Phase 3 popover anchored to the Resolve action area.
|
||||
*
|
||||
* Persistent (not modal) popover showing the four-section draft markdown that
|
||||
* would be posted to the customer ticket on Resolve. Per FLOWPILOT-MIGRATION.md
|
||||
* Section 3.1, the engineer reviews/edits the draft inline and Confirm & post
|
||||
* fires the PSA writeback (wired in Phase 4 — for now this is read-only).
|
||||
*
|
||||
* Refresh policy: parent triggers `onRefresh` when state_version changes.
|
||||
* Backend caches by state_version, so repeat fetches are cheap (no Sonnet
|
||||
* call) when no facts/fixes/scripts have changed.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Loader2, RefreshCw, X, FileText } from 'lucide-react'
|
||||
import { MarkdownContent } from '@/components/ui/MarkdownContent'
|
||||
import type { ResolutionNotePreview as PreviewData } from '@/api/sessionSuggestedFixes'
|
||||
|
||||
interface ResolutionNotePreviewProps {
|
||||
open: boolean
|
||||
loading: boolean
|
||||
preview: PreviewData | null
|
||||
error: string | null
|
||||
onClose: () => void
|
||||
onRefresh: () => Promise<void> | void
|
||||
}
|
||||
|
||||
export function ResolutionNotePreview({
|
||||
open,
|
||||
loading,
|
||||
preview,
|
||||
error,
|
||||
onClose,
|
||||
onRefresh,
|
||||
}: ResolutionNotePreviewProps) {
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
if (!open) return null
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
try { await onRefresh() } finally { setRefreshing(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
// The popover is positioned absolutely against its anchor by the parent.
|
||||
// We render full-width inside the task lane below the Resolve action bar.
|
||||
<div className="rounded-lg border border-default bg-elevated/30 mx-3 mb-3 overflow-hidden shadow-lg">
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-default bg-bg-page">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText size={13} className="text-accent" />
|
||||
<span className="text-[0.75rem] font-semibold text-heading">
|
||||
Resolution note preview
|
||||
</span>
|
||||
{preview?.target_ticket_ref && (
|
||||
<span className="text-[0.6875rem] font-mono text-accent-text">
|
||||
→ {preview.target_ticket_ref}
|
||||
</span>
|
||||
)}
|
||||
{preview?.from_cache && (
|
||||
<span className="text-[0.6875rem] text-muted-foreground italic">cached</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing || loading}
|
||||
className="p-1 rounded text-muted-foreground hover:text-heading hover:bg-elevated/40 transition-colors disabled:opacity-40"
|
||||
title="Refresh preview"
|
||||
>
|
||||
{refreshing ? <Loader2 size={11} className="animate-spin" /> : <RefreshCw size={11} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded text-muted-foreground hover:text-heading hover:bg-elevated/40 transition-colors"
|
||||
title="Close preview"
|
||||
>
|
||||
<X size={11} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-3 max-h-[40vh] overflow-y-auto">
|
||||
{loading && !preview && (
|
||||
<div className="flex items-center gap-2 text-[0.75rem] text-muted-foreground">
|
||||
<Loader2 size={12} className="animate-spin" />
|
||||
Drafting note from session state...
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-[0.75rem] text-danger">{error}</div>
|
||||
)}
|
||||
{preview && (
|
||||
<div className="prose prose-invert prose-sm max-w-none text-[0.8125rem] leading-relaxed">
|
||||
<MarkdownContent content={preview.markdown} />
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && !preview && (
|
||||
<div className="text-[0.75rem] text-muted-foreground italic">
|
||||
No preview yet — add a fact or accept a suggested fix to populate.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResolutionNotePreview
|
||||
82
frontend/src/components/pilot/sections/SuggestedFix.tsx
Normal file
82
frontend/src/components/pilot/sections/SuggestedFix.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* SuggestedFix card — Phase 3 task-lane section.
|
||||
*
|
||||
* Renders the active AI-proposed resolution path for the session
|
||||
* (per FLOWPILOT-MIGRATION.md Section 3.1, "Suggested fix"). Amber-accented
|
||||
* to match the mockup; clicking opens the Script Generator flow in Phase 5.
|
||||
*
|
||||
* For Phase 3, the card is informational + a Dismiss action. The three-option
|
||||
* dialog (one_off / draft_template / build_template) is wired in Phase 5
|
||||
* via a separate component.
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { Sparkles, X } from 'lucide-react'
|
||||
import type { SessionSuggestedFix } from '@/api/sessionSuggestedFixes'
|
||||
|
||||
interface SuggestedFixProps {
|
||||
fix: SessionSuggestedFix
|
||||
onDismiss: () => Promise<void> | void
|
||||
}
|
||||
|
||||
function confidenceBucket(pct: number): { label: string; tone: string } {
|
||||
if (pct >= 80) return { label: 'high', tone: 'text-success' }
|
||||
if (pct >= 50) return { label: 'medium', tone: 'text-warning' }
|
||||
return { label: 'low', tone: 'text-muted-foreground' }
|
||||
}
|
||||
|
||||
export function SuggestedFix({ fix, onDismiss }: SuggestedFixProps) {
|
||||
const [busy, setBusy] = useState(false)
|
||||
const conf = confidenceBucket(fix.confidence_pct)
|
||||
|
||||
const handleDismiss = async () => {
|
||||
setBusy(true)
|
||||
try { await onDismiss() } finally { setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="sticky top-0 z-10 pb-2" style={{ background: 'var(--color-bg-page)' }}>
|
||||
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[1.2px] text-muted-foreground pl-0.5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-warning" />
|
||||
Suggested fix
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className={`tabular-nums ${conf.tone}`}>{fix.confidence_pct}% confidence</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-l-[3px] border-l-warning border border-warning/25 bg-warning-dim/15 p-3 mb-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles size={14} className="text-warning shrink-0 mt-0.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[0.8125rem] font-medium text-heading leading-snug">
|
||||
{fix.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[0.75rem] text-muted-foreground leading-relaxed">
|
||||
{fix.description}
|
||||
</div>
|
||||
{fix.script_template_id && (
|
||||
<div className="mt-1.5 text-[0.6875rem] text-success">
|
||||
✓ Matches an existing Script Library template
|
||||
</div>
|
||||
)}
|
||||
{!fix.script_template_id && fix.ai_drafted_script && (
|
||||
<div className="mt-1.5 text-[0.6875rem] text-accent-text">
|
||||
Custom script drafted (no template match)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
disabled={busy}
|
||||
className="p-1 rounded text-muted-foreground hover:text-danger hover:bg-elevated/40 transition-colors"
|
||||
title="Dismiss this suggestion"
|
||||
>
|
||||
<X size={11} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuggestedFix
|
||||
Reference in New Issue
Block a user