diff --git a/cli/src/types.ts b/cli/src/types.ts
index c366fa42..f2b7c7ff 100644
--- a/cli/src/types.ts
+++ b/cli/src/types.ts
@@ -427,3 +427,16 @@ export interface DispatchResponse {
};
}
+export interface DispatchImagePromptRequest {
+ title: string;
+ tags: string[];
+ tldr: string;
+ format: DispatchFormat;
+}
+
+export interface DispatchImagePromptResponse {
+ prompt: string;
+ model: string;
+ tokensUsed: { input: number; output: number };
+}
+
diff --git a/dashboard/src/components/dispatch/CoverImagePromptSection.tsx b/dashboard/src/components/dispatch/CoverImagePromptSection.tsx
new file mode 100644
index 00000000..8b935f1e
--- /dev/null
+++ b/dashboard/src/components/dispatch/CoverImagePromptSection.tsx
@@ -0,0 +1,101 @@
+import { useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import { Sparkles, Loader2, AlertCircle, Copy, Check } from 'lucide-react';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button';
+import { Textarea } from '@/components/ui/textarea';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { generateDispatchImagePrompt } from '@/lib/api';
+import type { DispatchFormat } from '@/lib/api';
+
+interface CoverImagePromptSectionProps {
+ title: string;
+ tags: string[];
+ tldr: string;
+ format: DispatchFormat;
+}
+
+export function CoverImagePromptSection({ title, tags, tldr, format }: CoverImagePromptSectionProps) {
+ const [copied, setCopied] = useState(false);
+
+ const mutation = useMutation({
+ mutationFn: () => generateDispatchImagePrompt({ title, tags, tldr, format }),
+ });
+
+ function handleCopy() {
+ if (!mutation.data) return;
+ void navigator.clipboard.writeText(mutation.data.prompt).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ toast.success('Prompt copied to clipboard');
+ });
+ }
+
+ function handleRegenerate() {
+ mutation.reset();
+ mutation.mutate();
+ }
+
+ return (
+
+
Cover image prompt
+
+ {!mutation.data && !mutation.isPending && !mutation.isError && (
+
+ )}
+
+ {mutation.isPending && (
+
+ )}
+
+ {mutation.isError && (
+
+
+
+
+ {mutation.error instanceof Error ? mutation.error.message : 'Failed to generate prompt.'}
+
+
+
+
+ )}
+
+ {mutation.data && (
+
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/dashboard/src/components/dispatch/DispatchDrawer.tsx b/dashboard/src/components/dispatch/DispatchDrawer.tsx
index 841cf01a..86d5b76f 100644
--- a/dashboard/src/components/dispatch/DispatchDrawer.tsx
+++ b/dashboard/src/components/dispatch/DispatchDrawer.tsx
@@ -30,7 +30,7 @@ import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { generateDispatch } from '@/lib/api';
-import { PostPreview } from './PostPreview';
+import { PostOverlay } from './PostOverlay';
import type { Insight } from '@/lib/types';
import type { DispatchTone, DispatchFormat, DispatchResponse } from '@/lib/api';
@@ -123,10 +123,11 @@ export function DispatchDrawer({
const [tone, setTone] = useState('technical');
const [includeSessionBackground, setIncludeSessionBackground] = useState(false);
const [result, setResult] = useState(null);
+ const [overlayOpen, setOverlayOpen] = useState(false);
const mutation = useMutation({
mutationFn: generateDispatch,
- onSuccess: (data) => setResult(data),
+ onSuccess: (data) => { setResult(data); setOverlayOpen(true); },
});
const sensors = useSensors(
@@ -155,6 +156,7 @@ export function DispatchDrawer({
function handleClose() {
onOpenChange(false);
+ setOverlayOpen(false);
setResult(null);
mutation.reset();
setFormat('blog');
@@ -167,6 +169,7 @@ export function DispatchDrawer({
const contextTooLong = context.length > 500;
return (
+ <>
- {result ? (
-
- ) : (
-
+
{/* Selected insights with drag-to-reorder */}
@@ -330,10 +330,9 @@ export function DispatchDrawer({
)}
- )}
{/* Footer */}
- {!result && (
+ {!result ? (
- )}
-
- {result && (
-
+ ) : (
+
+