From 43cb97ac6d5cb67c3a9e6217d75e67a4fa51a3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 22 Mar 2026 14:33:47 -0400 Subject: [PATCH] feat(03-02): add SocialDraftPreview component and ApprovalInspector integration - Create SocialDraftPreview with platform icon, color, content, char count, media URLs, schedule display - Integrate SocialDraftPreview into ApprovalInspector for social_post approvals - Add schedule picker with immediate/scheduled radio group and datetime-local input - Add social_post to type badge with accent styling - Approve button shows 'Approve & Post' for social_post approvals - Accessibility: role=region, aria-label, aria-live=polite, role=radiogroup - Fix test type annotation for TS compatibility --- .../execution/ApprovalInspector.tsx | 131 +++++++++++++++++- .../execution/SocialDraftPreview.tsx | 131 ++++++++++++++++++ tests/unit/social-draft.test.ts | 2 +- 3 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 src/renderer/components/execution/SocialDraftPreview.tsx diff --git a/src/renderer/components/execution/ApprovalInspector.tsx b/src/renderer/components/execution/ApprovalInspector.tsx index 1b0df493..fe568251 100644 --- a/src/renderer/components/execution/ApprovalInspector.tsx +++ b/src/renderer/components/execution/ApprovalInspector.tsx @@ -1,11 +1,20 @@ import { useState } from "react" -import { Briefcase, CheckCircle, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react" +import { Briefcase, CheckCircle, Clock, ClockCounterClockwise, Robot, Trash, XCircle } from "@phosphor-icons/react" import type { AgentRecord, ApprovalRecord, TaskRecord } from "@shared/types" import { useT } from "../../i18n" import { formatMoney, formatTime } from "../../lib/formatters" import { unwrap } from "../../lib/desktop" import { ActionButton, ConfirmDialog, InlineNotice, StatusPill, TextArea } from "../ui" import { describeActionError, parseApprovalPayloadJson } from "./execution-utils" +import { SocialDraftPreview } from "./SocialDraftPreview" + +function getTypeBadgeClass(type: string): string { + if (type === "hire_agent") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" + if (type === "approve_ceo_strategy") return "bg-[color:var(--warn-soft)] text-[color:var(--warn)]" + if (type === "secret_access") return "bg-[color:var(--danger-soft)] text-[color:var(--danger)]" + if (type === "social_post") return "bg-[color:var(--accent-soft)] text-[color:var(--accent)]" + return "bg-[color:var(--panel-soft)] text-[color:var(--muted)]" +} export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide, onDeleted }: { approval: ApprovalRecord; agents: AgentRecord[]; tasks: TaskRecord[]; companyId: string @@ -14,12 +23,61 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide const t = useT() const [decisionNote, setDecisionNote] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false) const [actionError, setActionError] = useState(null); const [acting, setActing] = useState<"approved"|"rejected"|"revision_requested"|"delete"|null>(null) + const [scheduleMode, setScheduleMode] = useState<"immediate" | "scheduled">("immediate") + const [scheduledAt, setScheduledAt] = useState("") + const [scheduleError, setScheduleError] = useState(null) const requestingAgent = agents.find((a) => a.id === approval.requestedByAgentId) ?? null const relatedTask = tasks.find((t) => t.id === approval.relatedTaskId) ?? null const isDecided = approval.state !== "pending" + const isSocialPost = approval.type === "social_post" async function handleDelete() { setActionError(null); setActing("delete"); try { unwrap(await window.agentCompany.deleteApproval({ id: approval.id, companyId })); setConfirmDelete(false); await onDeleted() } catch (e) { setActionError(describeActionError(e)) } finally { setActing(null) } } - async function handleDecision(state: "approved"|"rejected"|"revision_requested") { setActionError(null); setActing(state); try { await onDecide(state, decisionNote || undefined) } catch (e) { setActionError(describeActionError(e)) } finally { setActing(null) } } + + async function handleDecision(state: "approved"|"rejected"|"revision_requested") { + setActionError(null) + setScheduleError(null) + + // Validate schedule if social_post and user picked "scheduled" + if (isSocialPost && state === "approved" && scheduleMode === "scheduled") { + if (!scheduledAt) { + setScheduleError("Please select a date and time for scheduling.") + return + } + const chosenTime = new Date(scheduledAt).getTime() + if (chosenTime <= Date.now()) { + setScheduleError("Scheduled time must be in the future.") + return + } + } + + setActing(state) + try { + // For social_post approvals with a schedule, include the schedule info in the decision note + let noteForDecision = decisionNote || undefined + if (isSocialPost && state === "approved" && scheduleMode === "scheduled" && scheduledAt) { + const scheduleInfo = `[scheduled:${new Date(scheduledAt).toISOString()}]` + noteForDecision = noteForDecision ? `${noteForDecision} ${scheduleInfo}` : scheduleInfo + } + await onDecide(state, noteForDecision) + } catch (e) { + setActionError(describeActionError(e)) + } finally { + setActing(null) + } + } + + // Compute minimum datetime for schedule picker: now + 5 minutes + const minDatetime = (() => { + const d = new Date(Date.now() + 5 * 60 * 1000) + // Format as YYYY-MM-DDTHH:mm for datetime-local input + const pad = (n: number) => String(n).padStart(2, "0") + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}` + })() + + const socialPlatform = (() => { + if (!isSocialPost || !approval.payloadJson) return "other" + try { return (JSON.parse(approval.payloadJson) as { platform?: string }).platform ?? "other" } catch { return "other" } + })() return (
@@ -27,7 +85,7 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide {actionError && !confirmDelete ? : null}
- {approval.type.replaceAll("_", " ")} + {approval.type.replaceAll("_", " ")}
{approval.payloadSummary}
@@ -49,10 +107,75 @@ export function ApprovalInspector({ approval, agents, tasks, companyId, onDecide {mgr ?
Reports to: {mgr.name}
: null}
) })() : null} {approval.type === "approve_ceo_strategy" ?
Organization Strategy
{approval.payloadSummary}
: null} + {isSocialPost && approval.payloadJson ? ( + + ) : null} + + {/* Schedule picker for social_post approvals */} + {isSocialPost && !isDecided ? ( +
+
+ + +
+ {scheduleMode === "scheduled" ? ( +
+ + { setScheduledAt(e.target.value); setScheduleError(null) }} + className="rounded-[8px] border border-[color:var(--line)] bg-[color:var(--panel)] px-3.5 py-2.5 text-[13px] text-[color:var(--text)] focus:border-[color:var(--accent)] focus:outline-none" + /> +
+ ) : null} + {scheduleError ? : null} +
+ ) : null} + {!isDecided ?