feat: inline Save Phase button in Atlas chat when phase is complete
Made-with: Cursor
This commit is contained in:
@@ -13,6 +13,29 @@ interface AtlasChatProps {
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase marker — Atlas appends [[PHASE_COMPLETE:{...}]] when a phase wraps up
|
||||
// ---------------------------------------------------------------------------
|
||||
const PHASE_MARKER_RE = /\[\[PHASE_COMPLETE:(.*?)\]\]/s;
|
||||
|
||||
interface PhasePayload {
|
||||
phase: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function extractPhase(text: string): { clean: string; phase: PhasePayload | null } {
|
||||
const match = text.match(PHASE_MARKER_RE);
|
||||
if (!match) return { clean: text, phase: null };
|
||||
try {
|
||||
const phase = JSON.parse(match[1]) as PhasePayload;
|
||||
return { clean: text.replace(PHASE_MARKER_RE, "").trimEnd(), phase };
|
||||
} catch {
|
||||
return { clean: text.replace(PHASE_MARKER_RE, "").trimEnd(), phase: null };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Markdown-lite renderer — handles **bold**, newlines, numbered/bullet lists
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -31,8 +54,29 @@ function renderContent(text: string | null | undefined) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Message row
|
||||
// ---------------------------------------------------------------------------
|
||||
function MessageRow({ msg, userInitial }: { msg: ChatMessage; userInitial: string }) {
|
||||
function MessageRow({ msg, userInitial, projectId }: { msg: ChatMessage; userInitial: string; projectId: string }) {
|
||||
const isAtlas = msg.role === "assistant";
|
||||
const { clean, phase } = isAtlas ? extractPhase(msg.content ?? "") : { clean: msg.content ?? "", phase: null };
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const handleSavePhase = async () => {
|
||||
if (!phase || saved || saving) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/save-phase`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(phase),
|
||||
});
|
||||
setSaved(true);
|
||||
} catch {
|
||||
// swallow — user can retry
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 12, marginBottom: 22, animation: "enter 0.3s ease both" }}>
|
||||
{/* Avatar */}
|
||||
@@ -61,8 +105,39 @@ function MessageRow({ msg, userInitial }: { msg: ChatMessage; userInitial: strin
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
whiteSpace: isAtlas ? "normal" : "pre-wrap",
|
||||
}}>
|
||||
{renderContent(msg.content)}
|
||||
{renderContent(clean)}
|
||||
</div>
|
||||
{/* Phase save button — only shown when Atlas signals phase completion */}
|
||||
{phase && (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<button
|
||||
onClick={handleSavePhase}
|
||||
disabled={saved || saving}
|
||||
style={{
|
||||
display: "inline-flex", alignItems: "center", gap: 7,
|
||||
padding: "8px 16px", borderRadius: 8,
|
||||
background: saved ? "#e8f5e9" : "#1a1a1a",
|
||||
color: saved ? "#2e7d32" : "#fff",
|
||||
border: saved ? "1px solid #a5d6a7" : "none",
|
||||
fontSize: "0.78rem", fontWeight: 600,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
cursor: saved || saving ? "default" : "pointer",
|
||||
transition: "all 0.15s",
|
||||
opacity: saving ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{saved ? "✓ Phase saved" : saving ? "Saving…" : `Save phase — ${phase.title}`}
|
||||
</button>
|
||||
{!saved && (
|
||||
<div style={{
|
||||
marginTop: 6, fontSize: "0.72rem", color: "#a09a90",
|
||||
fontFamily: "Outfit, sans-serif", lineHeight: 1.4,
|
||||
}}>
|
||||
{phase.summary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -261,7 +336,7 @@ export function AtlasChat({ projectId }: AtlasChatProps) {
|
||||
Reset
|
||||
</button>
|
||||
{visibleMessages.map((msg, i) => (
|
||||
<MessageRow key={i} msg={msg} userInitial={userInitial} />
|
||||
<MessageRow key={i} msg={msg} userInitial={userInitial} projectId={projectId} />
|
||||
))}
|
||||
{isStreaming && <TypingIndicator />}
|
||||
<div ref={endRef} />
|
||||
|
||||
Reference in New Issue
Block a user