Generated: July 2025 | Project: Content Generation Solution Accelerator
-
-
-
- 100%
- Original UI Files Impacted
-
-
- 4
- Redux Toolkit Slices Added
-
-
- -48.29%
- Original Components LOC Reduction
-
-
- +29.87%
- Net UI Delta Rate
-
-
-
-
-
-
-
📊 Executive Summary
-
Through comparison of origin/dev...psl-ui-refractoring in content-gen/src/app/frontend/src, this report captures measured UI refactorization impact with consistent diff-based KPIs.
-
-
48 UI files changed (100% of original dev UI files touched)
-
3,740 additions / 2,453 deletions (6,193 total churn)
-
+1,091 net lines in UI scope (+29.87% vs dev baseline)
-
Original components reduced by 1,130 lines (-48.29%)
-
4 Redux slices added with full store modularization
-
100% changed files are TypeScript (48/48 .ts/.tsx)
Monolithic components (App.tsx at 846 lines, ChatHistory at 616 lines) were decomposed into focused, single-responsibility components. 10 new granular components were extracted.
Pattern adoption increased for memoization, typed async flows, and Redux hooks.
-
All 18 components use displayName for DevTools identification.
-
All 18 components are wrapped with memo() for render optimization.
-
21 typed selectors centralized in store/selectors.ts.
-
-
-
-
-
-
-
-
📊 KPI 1: Codebase Overview
-
-
-
-
-
Metric
-
Before (dev)
-
After (local)
-
Delta
-
-
-
-
-
Total UI source files
-
13
-
47
-
+34 files
-
-
-
Total source lines
-
3,652
-
4,743
-
+1,091 (+29.87%)
-
-
-
Files added
-
—
-
35
-
Added
-
-
-
Files modified
-
—
-
12
-
Modified
-
-
-
Files deleted
-
—
-
1
-
Legacy API file removed
-
-
-
Line additions
-
—
-
3,740
-
+3,740
-
-
-
Line deletions
-
—
-
2,453
-
−2,453
-
-
-
-
-
-
-
-
📊 KPI 2: Component Complexity (Lines of Code)
-
-
-
-
-
Component
-
Before (lines)
-
After (lines)
-
Reduction
-
-
-
-
-
App.tsx
-
846
-
72
-
−774 (−91.49%)
-
-
-
ChatHistory.tsx
-
616
-
327
-
−289 (−46.92%)
-
-
-
InlineContentPreview.tsx
-
528
-
196
-
−332 (−62.88%)
-
-
-
ChatPanel.tsx
-
425
-
159
-
−266 (−62.59%)
-
-
-
api/index.ts
-
321
-
222
-
−99 (−30.84%)
-
-
-
ProductReview.tsx
-
217
-
128
-
−89 (−41.01%)
-
-
-
BriefReview.tsx
-
177
-
157
-
−20 (−11.30%)
-
-
-
WelcomeCard.tsx
-
154
-
103
-
−51 (−33.12%)
-
-
-
SelectedProductView.tsx
-
135
-
60
-
−75 (−55.56%)
-
-
-
ConfirmedBriefView.tsx
-
88
-
80
-
−8 (−9.09%)
-
-
-
-
-
Top Reduction Progress
-
-
-
App.tsx Reduction
-
-
−91.49%
-
-
-
-
InlineContentPreview.tsx Reduction
-
-
−62.88%
-
-
-
-
-
-
ChatPanel.tsx Reduction
-
-
−62.59%
-
-
-
-
SelectedProductView.tsx Reduction
-
-
−55.56%
-
-
-
-
-
-
ChatHistory.tsx Reduction
-
-
−46.92%
-
-
-
-
ProductReview.tsx Reduction
-
-
−41.01%
-
-
-
-
-
-
-
-
📊 KPI 3: Architecture & Modularity
-
-
-
-
-
Metric
-
Before
-
After
-
Delta
-
-
-
-
-
Custom hooks
-
0 files
-
7 files (797 lines)
-
+7 hook modules
-
-
-
State management files
-
0
-
8 files (501 lines)
-
+8 store modules
-
-
-
Utility modules
-
0
-
9 files (423 lines)
-
+9 under utils/
-
-
-
New components
-
0
-
10 files (1,220 lines)
-
+10 granular components
-
-
-
Typed selectors
-
0
-
21
-
+21 centralized in selectors.ts
-
-
-
-
-
New Files Created
-
-
-
State Management (8 files)
-
-
✅ store/index.ts
-
✅ store/store.ts
-
✅ store/hooks.ts
-
✅ store/selectors.ts
-
✅ store/appSlice.ts
-
✅ store/chatSlice.ts
-
✅ store/contentSlice.ts
-
✅ store/chatHistorySlice.ts
-
-
-
-
Custom Hooks (7 files)
-
-
✅ hooks/index.ts
-
✅ hooks/useAutoScroll.ts
-
✅ hooks/useChatOrchestrator.ts
-
✅ hooks/useContentGeneration.ts
-
✅ hooks/useConversationActions.ts
-
✅ hooks/useCopyToClipboard.ts
-
✅ hooks/useWindowSize.ts
-
-
-
-
-
-
Utility Modules (9 files)
-
-
✅ utils/index.ts
-
✅ utils/briefFields.ts
-
✅ utils/contentErrors.ts
-
✅ utils/contentParsing.ts
-
✅ utils/downloadImage.ts
-
✅ utils/generationStages.ts
-
✅ utils/messageUtils.ts
-
✅ utils/sseParser.ts
-
✅ utils/stringUtils.ts
-
-
-
-
New Components (10 files)
-
-
✅ components/AppHeader.tsx (65 lines)
-
✅ components/ChatInput.tsx (139 lines)
-
✅ components/ComplianceSection.tsx (187 lines)
-
✅ components/ConversationItem.tsx (276 lines)
-
✅ components/ImagePreviewCard.tsx (95 lines)
-
✅ components/MessageBubble.tsx (113 lines)
-
✅ components/ProductCard.tsx (132 lines)
-
✅ components/SuggestionCard.tsx (81 lines)
-
✅ components/TypingIndicator.tsx (71 lines)
-
✅ components/ViolationCard.tsx (61 lines)
-
-
-
-
-
-
-
-
📊 KPI 4: Bundle Size (Production Build)
-
-
-
-
-
Chunk
-
Before
-
After
-
Delta
-
-
-
-
-
Bundle KPI
-
Not measured
-
Not measured
-
Build benchmark required
-
-
-
Comparison basis
-
git diff origin/dev...HEAD in content-gen/src/app/frontend/src
-
Diff-based report
-
-
-
Measured churn
-
6,193 lines
-
3,740 add / 2,453 del
-
-
-
Net UI delta
-
+1,091 lines
-
+29.87% vs dev UI baseline
-
-
-
Status
-
Bundle size not included in this KPI set
-
Use build artifacts for bundle KPI
-
-
-
-
-
-
💡 Note: This report is strictly based on branch diff metrics. Bundle-size numbers require production build output from both refs and are intentionally excluded here.
-
-
-
-
-
-
📊 KPI 5: Code Quality Patterns
-
-
-
-
-
Pattern
-
Before
-
After
-
Delta
-
-
-
-
-
memo( component wrappers
-
0
-
18
-
+18 memoized component wrappers
-
-
-
useCallback(
-
13
-
26
-
+13 callback memoization
-
-
-
useMemo(
-
0
-
9
-
+9 derived value memoization
-
-
-
createSlice (Redux Toolkit)
-
0
-
4
-
+4 typed state modules
-
-
-
createAsyncThunk
-
0
-
6
-
+6 standardized async
-
-
-
useAppDispatch
-
0
-
13
-
+13 typed dispatch hooks
-
-
-
useAppSelector
-
0
-
48
-
+48 typed selector hooks
-
-
-
displayName
-
0
-
18
-
+18 DevTools identifiers
-
-
-
-
-
-
-
-
🐛 KPI 6: Structural Risk Reductions
-
-
-
-
-
#
-
Refactor Outcome
-
Evidence from Diff
-
Status
-
-
-
-
-
1
-
Monolith App.tsx decomposed
-
App.tsx reduced from 846 to 72 lines (−91.5%); logic extracted to hooks, store, and components
-
Completed
-
-
-
2
-
State flow centralized
-
Added 4 Redux slices, 21 selectors, and typed hooks under store/
-
- Action needed: This content has compliance issues that must be
- addressed before use. Please review the details in the Compliance Guidelines
- section below and regenerate with modifications, or manually edit the content to
- resolve the flagged items.
-
-
- ) : violations.length > 0 ? (
-
-
- Optional review: This content is approved but has minor
- suggestions for improvement. You can use it as-is or review the recommendations
- in the Compliance Guidelines section below.
-
-
- );
-});
-ViolationCard.displayName = 'ViolationCard';
diff --git a/src/App/src/components/WelcomeCard.tsx b/src/App/src/components/WelcomeCard.tsx
deleted file mode 100644
index b56b781c4..000000000
--- a/src/App/src/components/WelcomeCard.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { memo, useMemo } from 'react';
-import {
- Text,
- tokens,
-} from '@fluentui/react-components';
-import { SuggestionCard } from './SuggestionCard';
-import FirstPromptIcon from '../styles/images/firstprompt.png';
-import SecondPromptIcon from '../styles/images/secondprompt.png';
-
-interface SuggestionData {
- title: string;
- icon: string;
-}
-
-const suggestions: SuggestionData[] = [
- {
- title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.",
- icon: FirstPromptIcon,
- },
- {
- title: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.",
- icon: SecondPromptIcon,
- }
-];
-
-interface WelcomeCardProps {
- onSuggestionClick: (prompt: string) => void;
- currentInput?: string;
-}
-
-export const WelcomeCard = memo(function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) {
- const selectedIndex = useMemo(
- () => suggestions.findIndex(s => s.title === currentInput),
- [currentInput],
- );
-
- return (
-
- {/* Welcome card with suggestions inside */}
-
- {/* Header with icon and welcome message */}
-
-
- Welcome to your Content Generation Accelerator
-
-
- Here are the options I can assist you with today
-
-
+
+ {/* Chat History Sidebar - RIGHT side */}
+ {showChatHistory && (
+
+
+
+ )}
+
+
+ );
+}
+
+export default App;
diff --git a/src/app/frontend/src/api/index.ts b/src/app/frontend/src/api/index.ts
new file mode 100644
index 000000000..36781b77b
--- /dev/null
+++ b/src/app/frontend/src/api/index.ts
@@ -0,0 +1,252 @@
+/**
+ * API service for interacting with the Content Generation backend
+ */
+
+import type {
+ AgentResponse,
+ AppConfig,
+ MessageRequest,
+ MessageResponse,
+} from '../types';
+
+const API_BASE = '/api';
+
+/**
+ * Send a message or action to the /api/chat endpoint
+ */
+export async function sendMessage(
+ request: MessageRequest,
+ signal?: AbortSignal
+): Promise {
+ const response = await fetch(`${API_BASE}/chat`, {
+ signal,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(request),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Message request failed: ${response.statusText}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Get application configuration including feature flags
+ */
+export async function getAppConfig(): Promise {
+ const response = await fetch(`${API_BASE}/config`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to get config: ${response.statusText}`);
+ }
+
+ return response.json();
+}
+
+/**
+ * Request for content generation
+ */
+export interface GenerateRequest {
+ conversation_id: string;
+ user_id: string;
+ brief: Record;
+ products: Array>;
+ generate_images: boolean;
+}
+
+/**
+ * Generate content from a confirmed brief
+ */
+export async function* streamGenerateContent(
+ request: GenerateRequest,
+ signal?: AbortSignal
+): AsyncGenerator {
+ // Use polling-based approach for reliability with long-running tasks
+ const startResponse = await fetch(`${API_BASE}/generate/start`, {
+ signal,
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ brief: request.brief,
+ products: request.products || [],
+ generate_images: request.generate_images,
+ conversation_id: request.conversation_id,
+ user_id: request.user_id || 'anonymous',
+ }),
+ });
+
+ if (!startResponse.ok) {
+ throw new Error(`Content generation failed to start: ${startResponse.statusText}`);
+ }
+
+ const startData = await startResponse.json();
+ const taskId = startData.task_id;
+
+ console.log(`Generation started with task ID: ${taskId}`);
+
+ // Yield initial status
+ yield {
+ type: 'status',
+ content: 'Generation started...',
+ is_final: false,
+ } as AgentResponse;
+
+ // Poll for completion
+ let attempts = 0;
+ const maxAttempts = 600; // 10 minutes max with 1-second polling (image generation can take 3-5 min)
+ const pollInterval = 1000; // 1 second
+
+ while (attempts < maxAttempts) {
+ // Check if cancelled before waiting
+ if (signal?.aborted) {
+ throw new DOMException('Generation cancelled by user', 'AbortError');
+ }
+
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
+ attempts++;
+
+ // Check if cancelled after waiting
+ if (signal?.aborted) {
+ throw new DOMException('Generation cancelled by user', 'AbortError');
+ }
+
+ try {
+ const statusResponse = await fetch(`${API_BASE}/generate/status/${taskId}`, { signal });
+ if (!statusResponse.ok) {
+ throw new Error(`Failed to get task status: ${statusResponse.statusText}`);
+ }
+
+ const statusData = await statusResponse.json();
+
+ if (statusData.status === 'completed') {
+ // Yield the final result
+ yield {
+ type: 'agent_response',
+ content: JSON.stringify(statusData.result),
+ is_final: true,
+ } as AgentResponse;
+ return;
+ } else if (statusData.status === 'failed') {
+ throw new Error(statusData.error || 'Generation failed');
+ } else if (statusData.status === 'running') {
+ // Determine progress stage based on elapsed time
+ // Typical generation: 0-10s briefing, 10-25s copy, 25-45s image, 45-60s compliance
+ const elapsedSeconds = attempts;
+ let stage: number;
+ let stageMessage: string;
+
+ if (elapsedSeconds < 10) {
+ stage = 0;
+ stageMessage = 'Analyzing creative brief...';
+ } else if (elapsedSeconds < 25) {
+ stage = 1;
+ stageMessage = 'Generating marketing copy...';
+ } else if (elapsedSeconds < 35) {
+ stage = 2;
+ stageMessage = 'Creating image prompt...';
+ } else if (elapsedSeconds < 55) {
+ stage = 3;
+ stageMessage = 'Generating image with AI...';
+ } else if (elapsedSeconds < 70) {
+ stage = 4;
+ stageMessage = 'Running compliance check...';
+ } else {
+ stage = 5;
+ stageMessage = 'Finalizing content...';
+ }
+
+ // Send status update every second for smoother progress
+ yield {
+ type: 'heartbeat',
+ content: stageMessage,
+ count: stage,
+ elapsed: elapsedSeconds,
+ is_final: false,
+ } as AgentResponse;
+ }
+ } catch (error) {
+ console.error(`Error polling task ${taskId}:`, error);
+ // Continue polling on transient errors
+ if (attempts >= maxAttempts) {
+ throw error;
+ }
+ }
+ }
+
+ throw new Error('Generation timed out after 10 minutes');
+}
+
+/**
+ * Poll for task completion using task_id
+ * Used for both content generation and image regeneration
+ */
+export async function* pollTaskStatus(
+ taskId: string,
+ signal?: AbortSignal
+): AsyncGenerator {
+ let attempts = 0;
+ const maxAttempts = 600; // 10 minutes max with 1-second polling
+ const pollInterval = 1000; // 1 second
+
+ while (attempts < maxAttempts) {
+ if (signal?.aborted) {
+ throw new DOMException('Operation cancelled by user', 'AbortError');
+ }
+
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
+ attempts++;
+
+ if (signal?.aborted) {
+ throw new DOMException('Operation cancelled by user', 'AbortError');
+ }
+
+ try {
+ const statusResponse = await fetch(`${API_BASE}/generate/status/${taskId}`, { signal });
+ if (!statusResponse.ok) {
+ throw new Error(`Failed to get task status: ${statusResponse.statusText}`);
+ }
+
+ const statusData = await statusResponse.json();
+
+ if (statusData.status === 'completed') {
+ yield {
+ type: 'agent_response',
+ content: JSON.stringify(statusData.result),
+ is_final: true,
+ } as AgentResponse;
+ return;
+ } else if (statusData.status === 'failed') {
+ throw new Error(statusData.error || 'Task failed');
+ } else {
+ // Yield heartbeat with progress
+ const elapsedSeconds = attempts;
+ let stageMessage: string;
+
+ if (elapsedSeconds < 10) {
+ stageMessage = 'Starting regeneration...';
+ } else if (elapsedSeconds < 30) {
+ stageMessage = 'Generating new image...';
+ } else if (elapsedSeconds < 50) {
+ stageMessage = 'Processing image...';
+ } else {
+ stageMessage = 'Finalizing...';
+ }
+
+ yield {
+ type: 'heartbeat',
+ content: stageMessage,
+ elapsed: elapsedSeconds,
+ is_final: false,
+ } as AgentResponse;
+ }
+ } catch (error) {
+ if (attempts >= maxAttempts) {
+ throw error;
+ }
+ }
+ }
+
+ throw new Error('Task timed out after 10 minutes');
+}
\ No newline at end of file
diff --git a/src/App/src/components/BriefReview.tsx b/src/app/frontend/src/components/BriefReview.tsx
similarity index 72%
rename from src/App/src/components/BriefReview.tsx
rename to src/app/frontend/src/components/BriefReview.tsx
index b4a1ce1de..6ea755905 100644
--- a/src/App/src/components/BriefReview.tsx
+++ b/src/app/frontend/src/components/BriefReview.tsx
@@ -1,11 +1,9 @@
-import { memo, useMemo } from 'react';
import {
Button,
Text,
tokens,
} from '@fluentui/react-components';
import type { CreativeBrief } from '../types';
-import { BRIEF_FIELD_LABELS, BRIEF_DISPLAY_ORDER, BRIEF_FIELD_KEYS, AI_DISCLAIMER } from '../utils';
interface BriefReviewProps {
brief: CreativeBrief;
@@ -14,22 +12,47 @@ interface BriefReviewProps {
isAwaitingResponse?: boolean;
}
-export const BriefReview = memo(function BriefReview({
+// Mapping of field keys to user-friendly labels for the 9 key areas
+const fieldLabels: Record = {
+ overview: 'Overview',
+ objectives: 'Objectives',
+ target_audience: 'Target Audience',
+ key_message: 'Key Message',
+ tone_and_style: 'Tone and Style',
+ deliverable: 'Deliverable',
+ timelines: 'Timelines',
+ visual_guidelines: 'Visual Guidelines',
+ cta: 'Call to Action',
+};
+
+export function BriefReview({
brief,
onConfirm,
onStartOver,
isAwaitingResponse = false,
}: BriefReviewProps) {
- const { populatedFields, missingFields, populatedDisplayFields } = useMemo(() => {
- const populated = BRIEF_FIELD_KEYS.filter(key => brief[key]?.trim()).length;
- const missing = BRIEF_FIELD_KEYS.filter(key => !brief[key]?.trim());
+ const allFields: (keyof CreativeBrief)[] = [
+ 'overview', 'objectives', 'target_audience', 'key_message',
+ 'tone_and_style', 'deliverable', 'timelines', 'visual_guidelines', 'cta'
+ ];
+ const populatedFields = allFields.filter(key => brief[key]?.trim()).length;
+ const missingFields = allFields.filter(key => !brief[key]?.trim());
+
+ // Define the order and labels for display in the card
+ const displayOrder: { key: keyof CreativeBrief; label: string }[] = [
+ { key: 'overview', label: 'Campaign Objective' },
+ { key: 'objectives', label: 'Objectives' },
+ { key: 'target_audience', label: 'Target Audience' },
+ { key: 'key_message', label: 'Key Message' },
+ { key: 'tone_and_style', label: 'Tone & Style' },
+ { key: 'visual_guidelines', label: 'Visual Guidelines' },
+ { key: 'deliverable', label: 'Deliverables' },
+ { key: 'timelines', label: 'Timelines' },
+ { key: 'cta', label: 'Call to Action' },
+ ];
- return {
- populatedFields: populated,
- missingFields: missing,
- populatedDisplayFields: BRIEF_DISPLAY_ORDER.filter(({ key }) => brief[key]?.trim()),
- };
- }, [brief]);
+ // Filter to only populated fields
+ const populatedDisplayFields = displayOrder.filter(({ key }) => brief[key]?.trim());
return (
I've captured {populatedFields} of 9 key areas. Would you like to add more details?
- You are missing: {missingFields.map(f => BRIEF_FIELD_LABELS[f]).join(', ')}.
+ You are missing: {missingFields.map(f => fieldLabels[f]).join(', ')}.
+
+
+ {getErrorMessage(image_error || error).title}
+
+
+ Click Regenerate to try again
+
+
+ )}
+
+
+
+ {/* User guidance callout for compliance status */}
+ {requires_modification ? (
+
+
+ Action needed: This content has compliance issues that must be addressed before use.
+ Please review the details in the Compliance Guidelines section below and regenerate with modifications,
+ or manually edit the content to resolve the flagged items.
+
+
+ ) : violations.length > 0 ? (
+
+
+ Optional review: This content is approved but has minor suggestions for improvement.
+ You can use it as-is or review the recommendations in the Compliance Guidelines section below.
+
+
+ );
+}
diff --git a/src/app/frontend/src/components/WelcomeCard.tsx b/src/app/frontend/src/components/WelcomeCard.tsx
new file mode 100644
index 000000000..ab5740708
--- /dev/null
+++ b/src/app/frontend/src/components/WelcomeCard.tsx
@@ -0,0 +1,158 @@
+import {
+ Card,
+ Text,
+ tokens,
+} from '@fluentui/react-components';
+import FirstPromptIcon from '../styles/images/firstprompt.png';
+import SecondPromptIcon from '../styles/images/secondprompt.png';
+
+interface SuggestionCard {
+ title: string;
+ prompt: string;
+ icon: string;
+}
+
+const suggestions: SuggestionCard[] = [
+ {
+ title: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.",
+ prompt: "I need to create a social media post about paint products for home remodels. The campaign is titled \"Brighten Your Springtime\" and the audience is new homeowners. I need marketing copy plus an image. The image should be an informal living room with tasteful furnishings.",
+ icon: FirstPromptIcon,
+ },
+ {
+ title: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.",
+ prompt: "Generate a social media campaign with ad copy and an image. This is for \"Back to School\" and the audience is parents of school age children. Tone is playful and humorous. The image must have minimal kids accessories in a children's bedroom. Show the room in a wide view.",
+ icon: SecondPromptIcon,
+ }
+];
+
+interface WelcomeCardProps {
+ onSuggestionClick: (prompt: string) => void;
+ currentInput?: string;
+}
+
+export function WelcomeCard({ onSuggestionClick, currentInput = '' }: WelcomeCardProps) {
+ const selectedIndex = suggestions.findIndex(s => s.prompt === currentInput);
+ return (
+
+ {/* Welcome card with suggestions inside */}
+
+ {/* Header with icon and welcome message */}
+
+
+ Welcome to your Content Generation Accelerator
+
+
+ Here are the options I can assist you with today
+
+