fix: unblock clarification retries and refresh handoff UX
This commit is contained in:
+88
-20
@@ -143,6 +143,7 @@ function App() {
|
||||
const [triageError, setTriageError] = useState('')
|
||||
const [triageBatchStatus, setTriageBatchStatus] = useState<'idle' | 'running'>('idle')
|
||||
const [triageEditMode, setTriageEditMode] = useState(false)
|
||||
const [triageClarificationAnswer, setTriageClarificationAnswer] = useState('')
|
||||
const [triageSavedMessage, setTriageSavedMessage] = useState('')
|
||||
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
|
||||
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
|
||||
@@ -656,6 +657,7 @@ function App() {
|
||||
setTriageOpen(true)
|
||||
setTriageError('')
|
||||
setTriageEditMode(false)
|
||||
setTriageClarificationAnswer('')
|
||||
setTriageSavedMessage('')
|
||||
setTriageStatus(triageRecommendation && !seed ? 'ready' : 'idle')
|
||||
if (seed) {
|
||||
@@ -671,6 +673,7 @@ function App() {
|
||||
setTriageStatus('idle')
|
||||
setTriageError('')
|
||||
setTriageEditMode(false)
|
||||
setTriageClarificationAnswer('')
|
||||
}
|
||||
|
||||
const openDecisionPulse = (pulseId?: string | null) => {
|
||||
@@ -741,7 +744,7 @@ function App() {
|
||||
.map((pulse) => ({ message: pulse.message, evidence_refs: pulse.evidence_refs })),
|
||||
})
|
||||
|
||||
const runAiTriage = async () => {
|
||||
const runAiTriage = async (optionalContextOverride?: string) => {
|
||||
const rawIdea = triageDraft.rawIdea.trim()
|
||||
if (!rawIdea) {
|
||||
setTriageError('Write the idea first. The scope goblin needs bait.')
|
||||
@@ -749,6 +752,8 @@ function App() {
|
||||
return
|
||||
}
|
||||
|
||||
const optionalContext = optionalContextOverride ?? triageDraft.optionalContext.trim()
|
||||
|
||||
setTriageStatus('loading')
|
||||
setTriageError('')
|
||||
setTriageSavedMessage('')
|
||||
@@ -757,7 +762,7 @@ function App() {
|
||||
try {
|
||||
const recommendation = await triageIdeaWithAi({
|
||||
raw_idea: rawIdea,
|
||||
optional_context: triageDraft.optionalContext.trim(),
|
||||
optional_context: optionalContext,
|
||||
app_context: buildAiTriageContext(),
|
||||
})
|
||||
const timestamp = nowIso()
|
||||
@@ -765,7 +770,7 @@ function App() {
|
||||
id: `rec_${slugify(rawIdea)}_${timestamp.replace(/[^0-9]/g, '')}`,
|
||||
created_at: timestamp,
|
||||
raw_idea: rawIdea,
|
||||
optional_context: triageDraft.optionalContext.trim(),
|
||||
optional_context: optionalContext,
|
||||
context_summary: `${appState.project.name}: ${appState.project.current_goal}`,
|
||||
...recommendation,
|
||||
user_decision: 'pending',
|
||||
@@ -786,6 +791,7 @@ function App() {
|
||||
})
|
||||
setTriageStatus('ready')
|
||||
setTriageEditMode(false)
|
||||
setTriageClarificationAnswer('')
|
||||
setStatusMessage(`AI suggested ${placementLabels[fullRecommendation.suggested_placement]} with ${fullRecommendation.scope_risk} scope risk.`)
|
||||
} catch (error) {
|
||||
setTriageStatus('error')
|
||||
@@ -1203,6 +1209,28 @@ function App() {
|
||||
setStatusMessage('Pulse deleted.')
|
||||
}
|
||||
|
||||
const answerClarifyingQuestion = async () => {
|
||||
const answer = triageClarificationAnswer.trim()
|
||||
if (!answer) {
|
||||
setTriageError('Give the AI a real answer first.')
|
||||
setTriageStatus('error')
|
||||
return
|
||||
}
|
||||
|
||||
const mergedContext = [triageDraft.optionalContext.trim(), `Clarification answer: ${answer}`]
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
|
||||
setTriageDraft((current) => ({
|
||||
...current,
|
||||
optionalContext: mergedContext,
|
||||
}))
|
||||
setTriageRecommendation(null)
|
||||
setTriageEditMode(false)
|
||||
setStatusMessage('Re-running triage with your clarification…')
|
||||
await runAiTriage(mergedContext)
|
||||
}
|
||||
|
||||
const copyMarkdown = async (filename: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(markdownPackage[filename as keyof typeof markdownPackage])
|
||||
@@ -1274,8 +1302,10 @@ function App() {
|
||||
? `${target} handoff for “${feature.title}” copied${shouldLogIntent ? ' and INTENT logged' : ''}.`
|
||||
: `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`,
|
||||
)
|
||||
return true
|
||||
} catch {
|
||||
setStatusMessage('Clipboard copy failed. Browser said no.')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1292,9 +1322,15 @@ function App() {
|
||||
}
|
||||
|
||||
const syncHandoffPreviewTarget = (target: (typeof PROMPT_TARGETS)[number]) => {
|
||||
const { prompt } = buildHandoffPrompt(handoffPreviewFeatureId || undefined, target)
|
||||
const { prompt } = buildHandoffPrompt(handoffPreviewFeatureId || undefined, handoffPreviewTarget)
|
||||
if (handoffPreviewDraft.trim() !== prompt.trim()) {
|
||||
const shouldReplace = window.confirm('Switching target will replace your current edits with the new preset. Continue?')
|
||||
if (!shouldReplace) return
|
||||
}
|
||||
|
||||
const { prompt: nextPrompt } = buildHandoffPrompt(handoffPreviewFeatureId || undefined, target)
|
||||
setHandoffPreviewTarget(target)
|
||||
setHandoffPreviewDraft(prompt)
|
||||
setHandoffPreviewDraft(nextPrompt)
|
||||
}
|
||||
|
||||
const resetHandoffPreview = () => {
|
||||
@@ -1621,16 +1657,20 @@ function App() {
|
||||
|
||||
<div className="triage-step-card triage-decision-box">
|
||||
<p className="eyebrow">Step 3 of 3</p>
|
||||
<h3>Choose action</h3>
|
||||
<p>AI advises. You decide.</p>
|
||||
<h3>{triageNeedsClarification ? 'Clarify before deciding' : 'Choose action'}</h3>
|
||||
<p>{triageNeedsClarification ? 'Answer the question, then re-run triage with the missing detail.' : 'AI advises. You decide.'}</p>
|
||||
<div className="button-row triage-action-stack">
|
||||
{triageNeedsClarification ? (
|
||||
<>
|
||||
<button type="button" onClick={() => setTriageEditMode(true)}>Answer Question</button>
|
||||
<button type="button" className="primary" onClick={() => {
|
||||
setTriageEditMode(true)
|
||||
setStatusMessage('Answer the clarification question below, then re-run triage.')
|
||||
}}>Answer + Retry</button>
|
||||
<button type="button" className="ghost" onClick={() => {
|
||||
setTriageRecommendation(null)
|
||||
setTriageStatus('idle')
|
||||
setTriageEditMode(false)
|
||||
setTriageClarificationAnswer('')
|
||||
}}>Edit Idea</button>
|
||||
<button type="button" className="danger small" onClick={rejectTriageRecommendation}>Reject</button>
|
||||
</>
|
||||
@@ -1656,7 +1696,32 @@ function App() {
|
||||
|
||||
{triageEditMode && (
|
||||
<div className="triage-step-card triage-edit-fields form-grid">
|
||||
<p className="eyebrow">Edit before saving</p>
|
||||
<p className="eyebrow">{triageNeedsClarification ? 'Answer and refine' : 'Edit before saving'}</p>
|
||||
{triageNeedsClarification && (
|
||||
<>
|
||||
<div className="triage-callout warning full-span">
|
||||
<strong>Answer the question</strong>
|
||||
<p>{triageRecommendation?.clarifying_question || 'Clarify the idea, then re-run triage.'}</p>
|
||||
</div>
|
||||
<label className="full-span">
|
||||
Your answer
|
||||
<textarea
|
||||
rows={4}
|
||||
value={triageClarificationAnswer}
|
||||
onChange={(event) => setTriageClarificationAnswer(event.target.value)}
|
||||
placeholder="Add the missing detail so the AI can classify the idea properly."
|
||||
/>
|
||||
</label>
|
||||
<div className="button-inline-row full-span">
|
||||
<button type="button" onClick={() => void answerClarifyingQuestion()} disabled={triageStatus === 'loading'}>
|
||||
{triageStatus === 'loading' ? 'Re-running…' : 'Re-run with Answer'}
|
||||
</button>
|
||||
<button type="button" className="ghost small" onClick={() => setTriageEditMode(false)}>
|
||||
Hide Clarification
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<label>
|
||||
Placement
|
||||
<select value={triageEditable.placement} onChange={(event) => setTriageEditable((current) => ({ ...current, placement: event.target.value as AiPlacement }))}>
|
||||
@@ -1734,7 +1799,7 @@ function App() {
|
||||
<button
|
||||
key={target}
|
||||
type="button"
|
||||
className={handoffPreviewTarget === target ? 'small' : 'ghost small'}
|
||||
className={handoffPreviewTarget === target ? 'primary small' : 'ghost small'}
|
||||
onClick={() => syncHandoffPreviewTarget(target)}
|
||||
>
|
||||
{target}
|
||||
@@ -1742,6 +1807,7 @@ function App() {
|
||||
))}
|
||||
</div>
|
||||
<p className="tap-hint">Preview mode is the safer path when you want to edit wording or create a clean INTENT pulse.</p>
|
||||
<p className="tap-hint">Switching targets resets the draft to that target’s preset.</p>
|
||||
<div className="button-inline-row">
|
||||
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
|
||||
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(handoffPreviewFeature.id)}>Open in Export</button>
|
||||
@@ -1761,19 +1827,20 @@ function App() {
|
||||
<div className="button-row sheet-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, false, handoffPreviewDraft)
|
||||
setHandoffPreviewOpen(false)
|
||||
className="ghost"
|
||||
onClick={async () => {
|
||||
const ok = await copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, false, handoffPreviewDraft)
|
||||
if (ok) setHandoffPreviewOpen(false)
|
||||
}}
|
||||
>
|
||||
Copy Handoff
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={() => {
|
||||
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, true, handoffPreviewDraft)
|
||||
setHandoffPreviewOpen(false)
|
||||
className="primary"
|
||||
onClick={async () => {
|
||||
const ok = await copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, true, handoffPreviewDraft)
|
||||
if (ok) setHandoffPreviewOpen(false)
|
||||
}}
|
||||
>
|
||||
Copy + INTENT Pulse
|
||||
@@ -1848,13 +1915,14 @@ function App() {
|
||||
Log Feature Pulse
|
||||
</button>
|
||||
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
||||
Prepare AI Handoff
|
||||
Open in Export
|
||||
</button>
|
||||
<button type="button" className="ghost small" onClick={() => openHandoffPreview(selectedFeature.id, 'OpenClaw')}>
|
||||
<button type="button" className="primary small" onClick={() => openHandoffPreview(selectedFeature.id, 'OpenClaw')}>
|
||||
Preview / Edit Handoff
|
||||
</button>
|
||||
</div>
|
||||
<p className="tap-hint">Quick copy if you already know the target. Use preview when you want to tweak the brief or log an INTENT cleanly.</p>
|
||||
<p className="tap-hint">Preview is the safer path. Quick-copy is for when you already know the target and do not need edits.</p>
|
||||
<p className="tap-hint">Quick copy targets</p>
|
||||
<div className="button-inline-row">
|
||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'OpenClaw', false)}>
|
||||
Copy for OpenClaw
|
||||
|
||||
+23
-6
@@ -325,26 +325,43 @@ textarea {
|
||||
button,
|
||||
.import-label {
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #60a5fa, #818cf8);
|
||||
color: white;
|
||||
padding: 0.8rem 1rem;
|
||||
transition: transform 120ms ease, opacity 120ms ease;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.2);
|
||||
transition: transform 120ms ease, opacity 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.import-label:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background: linear-gradient(135deg, #67e8f9, #818cf8);
|
||||
color: #06111c;
|
||||
}
|
||||
|
||||
button.ghost,
|
||||
.import-label {
|
||||
background: rgba(30, 41, 59, 0.84);
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: rgba(18, 25, 40, 0.88);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
color: #dbe7ff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button.ghost:hover,
|
||||
.import-label:hover {
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22);
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: linear-gradient(135deg, #f97316, #ef4444);
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.small {
|
||||
@@ -946,7 +963,7 @@ body::before {
|
||||
overscroll-behavior-x: contain;
|
||||
scrollbar-width: none;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 999px;
|
||||
border-radius: 18px;
|
||||
background: rgba(3, 7, 18, 0.78);
|
||||
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(18px);
|
||||
@@ -958,7 +975,7 @@ body::before {
|
||||
|
||||
.tab {
|
||||
min-width: 8.5rem;
|
||||
border-radius: 999px;
|
||||
border-radius: 14px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
color: #9fb0c9;
|
||||
|
||||
Reference in New Issue
Block a user