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 [triageError, setTriageError] = useState('')
|
||||||
const [triageBatchStatus, setTriageBatchStatus] = useState<'idle' | 'running'>('idle')
|
const [triageBatchStatus, setTriageBatchStatus] = useState<'idle' | 'running'>('idle')
|
||||||
const [triageEditMode, setTriageEditMode] = useState(false)
|
const [triageEditMode, setTriageEditMode] = useState(false)
|
||||||
|
const [triageClarificationAnswer, setTriageClarificationAnswer] = useState('')
|
||||||
const [triageSavedMessage, setTriageSavedMessage] = useState('')
|
const [triageSavedMessage, setTriageSavedMessage] = useState('')
|
||||||
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
|
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
|
||||||
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
|
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
|
||||||
@@ -656,6 +657,7 @@ function App() {
|
|||||||
setTriageOpen(true)
|
setTriageOpen(true)
|
||||||
setTriageError('')
|
setTriageError('')
|
||||||
setTriageEditMode(false)
|
setTriageEditMode(false)
|
||||||
|
setTriageClarificationAnswer('')
|
||||||
setTriageSavedMessage('')
|
setTriageSavedMessage('')
|
||||||
setTriageStatus(triageRecommendation && !seed ? 'ready' : 'idle')
|
setTriageStatus(triageRecommendation && !seed ? 'ready' : 'idle')
|
||||||
if (seed) {
|
if (seed) {
|
||||||
@@ -671,6 +673,7 @@ function App() {
|
|||||||
setTriageStatus('idle')
|
setTriageStatus('idle')
|
||||||
setTriageError('')
|
setTriageError('')
|
||||||
setTriageEditMode(false)
|
setTriageEditMode(false)
|
||||||
|
setTriageClarificationAnswer('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDecisionPulse = (pulseId?: string | null) => {
|
const openDecisionPulse = (pulseId?: string | null) => {
|
||||||
@@ -741,7 +744,7 @@ function App() {
|
|||||||
.map((pulse) => ({ message: pulse.message, evidence_refs: pulse.evidence_refs })),
|
.map((pulse) => ({ message: pulse.message, evidence_refs: pulse.evidence_refs })),
|
||||||
})
|
})
|
||||||
|
|
||||||
const runAiTriage = async () => {
|
const runAiTriage = async (optionalContextOverride?: string) => {
|
||||||
const rawIdea = triageDraft.rawIdea.trim()
|
const rawIdea = triageDraft.rawIdea.trim()
|
||||||
if (!rawIdea) {
|
if (!rawIdea) {
|
||||||
setTriageError('Write the idea first. The scope goblin needs bait.')
|
setTriageError('Write the idea first. The scope goblin needs bait.')
|
||||||
@@ -749,6 +752,8 @@ function App() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const optionalContext = optionalContextOverride ?? triageDraft.optionalContext.trim()
|
||||||
|
|
||||||
setTriageStatus('loading')
|
setTriageStatus('loading')
|
||||||
setTriageError('')
|
setTriageError('')
|
||||||
setTriageSavedMessage('')
|
setTriageSavedMessage('')
|
||||||
@@ -757,7 +762,7 @@ function App() {
|
|||||||
try {
|
try {
|
||||||
const recommendation = await triageIdeaWithAi({
|
const recommendation = await triageIdeaWithAi({
|
||||||
raw_idea: rawIdea,
|
raw_idea: rawIdea,
|
||||||
optional_context: triageDraft.optionalContext.trim(),
|
optional_context: optionalContext,
|
||||||
app_context: buildAiTriageContext(),
|
app_context: buildAiTriageContext(),
|
||||||
})
|
})
|
||||||
const timestamp = nowIso()
|
const timestamp = nowIso()
|
||||||
@@ -765,7 +770,7 @@ function App() {
|
|||||||
id: `rec_${slugify(rawIdea)}_${timestamp.replace(/[^0-9]/g, '')}`,
|
id: `rec_${slugify(rawIdea)}_${timestamp.replace(/[^0-9]/g, '')}`,
|
||||||
created_at: timestamp,
|
created_at: timestamp,
|
||||||
raw_idea: rawIdea,
|
raw_idea: rawIdea,
|
||||||
optional_context: triageDraft.optionalContext.trim(),
|
optional_context: optionalContext,
|
||||||
context_summary: `${appState.project.name}: ${appState.project.current_goal}`,
|
context_summary: `${appState.project.name}: ${appState.project.current_goal}`,
|
||||||
...recommendation,
|
...recommendation,
|
||||||
user_decision: 'pending',
|
user_decision: 'pending',
|
||||||
@@ -786,6 +791,7 @@ function App() {
|
|||||||
})
|
})
|
||||||
setTriageStatus('ready')
|
setTriageStatus('ready')
|
||||||
setTriageEditMode(false)
|
setTriageEditMode(false)
|
||||||
|
setTriageClarificationAnswer('')
|
||||||
setStatusMessage(`AI suggested ${placementLabels[fullRecommendation.suggested_placement]} with ${fullRecommendation.scope_risk} scope risk.`)
|
setStatusMessage(`AI suggested ${placementLabels[fullRecommendation.suggested_placement]} with ${fullRecommendation.scope_risk} scope risk.`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setTriageStatus('error')
|
setTriageStatus('error')
|
||||||
@@ -1203,6 +1209,28 @@ function App() {
|
|||||||
setStatusMessage('Pulse deleted.')
|
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) => {
|
const copyMarkdown = async (filename: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(markdownPackage[filename as keyof typeof markdownPackage])
|
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 for “${feature.title}” copied${shouldLogIntent ? ' and INTENT logged' : ''}.`
|
||||||
: `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`,
|
: `${target} handoff copied${shouldLogIntent ? ' and INTENT logged' : ''}.`,
|
||||||
)
|
)
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
setStatusMessage('Clipboard copy failed. Browser said no.')
|
setStatusMessage('Clipboard copy failed. Browser said no.')
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1292,9 +1322,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const syncHandoffPreviewTarget = (target: (typeof PROMPT_TARGETS)[number]) => {
|
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)
|
setHandoffPreviewTarget(target)
|
||||||
setHandoffPreviewDraft(prompt)
|
setHandoffPreviewDraft(nextPrompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetHandoffPreview = () => {
|
const resetHandoffPreview = () => {
|
||||||
@@ -1621,16 +1657,20 @@ function App() {
|
|||||||
|
|
||||||
<div className="triage-step-card triage-decision-box">
|
<div className="triage-step-card triage-decision-box">
|
||||||
<p className="eyebrow">Step 3 of 3</p>
|
<p className="eyebrow">Step 3 of 3</p>
|
||||||
<h3>Choose action</h3>
|
<h3>{triageNeedsClarification ? 'Clarify before deciding' : 'Choose action'}</h3>
|
||||||
<p>AI advises. You decide.</p>
|
<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">
|
<div className="button-row triage-action-stack">
|
||||||
{triageNeedsClarification ? (
|
{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={() => {
|
<button type="button" className="ghost" onClick={() => {
|
||||||
setTriageRecommendation(null)
|
setTriageRecommendation(null)
|
||||||
setTriageStatus('idle')
|
setTriageStatus('idle')
|
||||||
setTriageEditMode(false)
|
setTriageEditMode(false)
|
||||||
|
setTriageClarificationAnswer('')
|
||||||
}}>Edit Idea</button>
|
}}>Edit Idea</button>
|
||||||
<button type="button" className="danger small" onClick={rejectTriageRecommendation}>Reject</button>
|
<button type="button" className="danger small" onClick={rejectTriageRecommendation}>Reject</button>
|
||||||
</>
|
</>
|
||||||
@@ -1656,7 +1696,32 @@ function App() {
|
|||||||
|
|
||||||
{triageEditMode && (
|
{triageEditMode && (
|
||||||
<div className="triage-step-card triage-edit-fields form-grid">
|
<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>
|
<label>
|
||||||
Placement
|
Placement
|
||||||
<select value={triageEditable.placement} onChange={(event) => setTriageEditable((current) => ({ ...current, placement: event.target.value as AiPlacement }))}>
|
<select value={triageEditable.placement} onChange={(event) => setTriageEditable((current) => ({ ...current, placement: event.target.value as AiPlacement }))}>
|
||||||
@@ -1734,7 +1799,7 @@ function App() {
|
|||||||
<button
|
<button
|
||||||
key={target}
|
key={target}
|
||||||
type="button"
|
type="button"
|
||||||
className={handoffPreviewTarget === target ? 'small' : 'ghost small'}
|
className={handoffPreviewTarget === target ? 'primary small' : 'ghost small'}
|
||||||
onClick={() => syncHandoffPreviewTarget(target)}
|
onClick={() => syncHandoffPreviewTarget(target)}
|
||||||
>
|
>
|
||||||
{target}
|
{target}
|
||||||
@@ -1742,6 +1807,7 @@ function App() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">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">
|
<div className="button-inline-row">
|
||||||
<button type="button" className="ghost small" onClick={resetHandoffPreview}>Reset to preset</button>
|
<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>
|
<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">
|
<div className="button-row sheet-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
className="ghost"
|
||||||
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, false, handoffPreviewDraft)
|
onClick={async () => {
|
||||||
setHandoffPreviewOpen(false)
|
const ok = await copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, false, handoffPreviewDraft)
|
||||||
|
if (ok) setHandoffPreviewOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy Handoff
|
Copy Handoff
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ghost"
|
className="primary"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
void copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, true, handoffPreviewDraft)
|
const ok = await copyFocusedHandoff(handoffPreviewFeature.id, handoffPreviewTarget, true, handoffPreviewDraft)
|
||||||
setHandoffPreviewOpen(false)
|
if (ok) setHandoffPreviewOpen(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Copy + INTENT Pulse
|
Copy + INTENT Pulse
|
||||||
@@ -1848,13 +1915,14 @@ function App() {
|
|||||||
Log Feature Pulse
|
Log Feature Pulse
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
<button type="button" className="ghost small" onClick={() => openFeatureHandoff(selectedFeature.id)}>
|
||||||
Prepare AI Handoff
|
Open in Export
|
||||||
</button>
|
</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
|
Preview / Edit Handoff
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="button-inline-row">
|
||||||
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'OpenClaw', false)}>
|
<button type="button" className="ghost small" onClick={() => void copyFocusedHandoff(selectedFeature.id, 'OpenClaw', false)}>
|
||||||
Copy for OpenClaw
|
Copy for OpenClaw
|
||||||
|
|||||||
+23
-6
@@ -325,26 +325,43 @@ textarea {
|
|||||||
button,
|
button,
|
||||||
.import-label {
|
.import-label {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 14px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(135deg, #60a5fa, #818cf8);
|
background: linear-gradient(135deg, #60a5fa, #818cf8);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.8rem 1rem;
|
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,
|
button:hover,
|
||||||
.import-label:hover {
|
.import-label:hover {
|
||||||
transform: translateY(-1px);
|
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,
|
button.ghost,
|
||||||
.import-label {
|
.import-label {
|
||||||
background: rgba(30, 41, 59, 0.84);
|
background: rgba(18, 25, 40, 0.88);
|
||||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
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 {
|
button.danger {
|
||||||
background: linear-gradient(135deg, #f97316, #ef4444);
|
background: linear-gradient(135deg, #f97316, #ef4444);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.small {
|
button.small {
|
||||||
@@ -946,7 +963,7 @@ body::before {
|
|||||||
overscroll-behavior-x: contain;
|
overscroll-behavior-x: contain;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
border-radius: 999px;
|
border-radius: 18px;
|
||||||
background: rgba(3, 7, 18, 0.78);
|
background: rgba(3, 7, 18, 0.78);
|
||||||
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);
|
box-shadow: 0 18px 36px rgba(0, 0, 0, 0.28);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
@@ -958,7 +975,7 @@ body::before {
|
|||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
min-width: 8.5rem;
|
min-width: 8.5rem;
|
||||||
border-radius: 999px;
|
border-radius: 14px;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #9fb0c9;
|
color: #9fb0c9;
|
||||||
|
|||||||
Reference in New Issue
Block a user