Files
memind/src/components/GoalRunAwaitingBanner.tsx
T
john 666db0b939 feat(goal-run): add multi-checkpoint goal orchestration with H5 and admin surfaces.
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval
UX in chat, and add admin inspection routes with local verify scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 17:03:16 +08:00

133 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { cancelGoalRun, type GoalRunAwaitingItem } from '../api/goalRun';
type GoalRunAwaitingBannerProps = {
items: GoalRunAwaitingItem[];
busy: boolean;
onContinue: (input: {
goalRunId: string;
checkpointId: string;
feedback?: string | null;
}) => Promise<void>;
onDismiss: (goalRunId: string) => void;
onRefresh: () => Promise<void>;
};
function GoalRunAwaitingCard({
item,
busy,
onContinue,
onDismiss,
onRefresh,
}: {
item: GoalRunAwaitingItem;
busy: boolean;
onContinue: GoalRunAwaitingBannerProps['onContinue'];
onDismiss: (goalRunId: string) => void;
onRefresh: () => Promise<void>;
}) {
const [feedback, setFeedback] = useState('');
const [action, setAction] = useState<'continue' | 'cancel' | null>(null);
const [localError, setLocalError] = useState<string | null>(null);
const { goal, checkpoint } = item;
const summary = checkpoint.outputSummary?.trim();
const handleContinue = async () => {
setLocalError(null);
setAction('continue');
try {
await onContinue({
goalRunId: goal.id,
checkpointId: checkpoint.id,
feedback: feedback.trim() || null,
});
onDismiss(goal.id);
} catch (err) {
setLocalError(err instanceof Error ? err.message : String(err));
await onRefresh();
} finally {
setAction(null);
}
};
const handleCancel = async () => {
setLocalError(null);
setAction('cancel');
try {
await cancelGoalRun(goal.id);
onDismiss(goal.id);
} catch (err) {
setLocalError(err instanceof Error ? err.message : String(err));
await onRefresh();
} finally {
setAction(null);
}
};
const cardBusy = busy || action != null;
return (
<div className="banner banner-info goal-run-awaiting-banner">
<div className="goal-run-awaiting-banner-body">
<strong>{goal.title}</strong>
<span>
阶段「{checkpoint.title}」已完成,请确认后继续下一阶段。
</span>
{summary ? <span className="goal-run-awaiting-summary">{summary}</span> : null}
<textarea
className="goal-run-awaiting-feedback"
rows={2}
placeholder="可选:补充反馈或修改方向"
value={feedback}
disabled={cardBusy}
onChange={(event) => setFeedback(event.target.value)}
/>
{localError ? <span className="goal-run-awaiting-error">{localError}</span> : null}
</div>
<div className="goal-run-awaiting-actions">
<button
type="button"
className="banner-action"
disabled={cardBusy}
onClick={() => void handleContinue()}
>
{action === 'continue' ? '处理中…' : '确认并继续'}
</button>
<button
type="button"
className="banner-dismiss"
disabled={cardBusy}
onClick={() => void handleCancel()}
>
{action === 'cancel' ? '取消中…' : '取消目标'}
</button>
</div>
</div>
);
}
export function GoalRunAwaitingBanner({
items,
busy,
onContinue,
onDismiss,
onRefresh,
}: GoalRunAwaitingBannerProps) {
if (!items.length) return null;
return (
<div className="goal-run-awaiting-stack">
{items.map((item) => (
<GoalRunAwaitingCard
key={item.goal.id}
item={item}
busy={busy}
onContinue={onContinue}
onDismiss={onDismiss}
onRefresh={onRefresh}
/>
))}
</div>
);
}