diff --git a/src/components/ThoughtStream.tsx b/src/components/ThoughtStream.tsx
index 8ac1adc..60b28a4 100644
--- a/src/components/ThoughtStream.tsx
+++ b/src/components/ThoughtStream.tsx
@@ -1,11 +1,12 @@
import { useEffect, useRef, useMemo, createElement, type ReactNode } from 'react';
-import type { ThoughtBubble, PrepareResult, GenerationPhase } from '../types';
+import type { ThoughtBubble, PrepareResult, GenerationPhase, AgentLoopState } from '../types';
interface ThoughtStreamProps {
thoughts: ThoughtBubble[];
phase: GenerationPhase;
elapsed: number;
prepareResult: PrepareResult | null;
+ agentLoop: AgentLoopState;
}
function formatTime(s: number) {
@@ -103,7 +104,73 @@ function MarkdownContent({ text }: { text: string }) {
return createElement('div', { className: 'text-xs text-gtext-secondary dark:text-gtext-secondary-dark space-y-0.5' }, ...elements);
}
-export default function ThoughtStream({ thoughts, phase, elapsed, prepareResult }: ThoughtStreamProps) {
+function getPhaseIcon(status: AgentLoopState['phases'][number]['status']) {
+ if (status === 'complete') return 'check_circle';
+ if (status === 'active') return 'progress_activity';
+ return 'radio_button_unchecked';
+}
+
+function getPhaseClass(status: AgentLoopState['phases'][number]['status']) {
+ if (status === 'complete') return 'text-gsuccess';
+ if (status === 'active') return 'text-gblue-600 dark:text-gblue-300';
+ return 'text-gtext-secondary/50 dark:text-gtext-secondary-dark/50';
+}
+
+function LoopStatus({ agentLoop }: { agentLoop: AgentLoopState }) {
+ const activePhase = agentLoop.phases.find(phase => phase.status === 'active');
+
+ return (
+
+ );
+}
+
+export default function ThoughtStream({ thoughts, phase, elapsed, prepareResult, agentLoop }: ThoughtStreamProps) {
const scrollRef = useRef
(null);
useEffect(() => {
@@ -118,7 +185,7 @@ export default function ThoughtStream({ thoughts, phase, elapsed, prepareResult
psychology
- AI Thinking
+ Agent Loop
timer
@@ -126,6 +193,8 @@ export default function ThoughtStream({ thoughts, phase, elapsed, prepareResult
+
+
{/* Thought bubbles scrollable area */}
= [
+ { id: 'intake', label: 'Intake', detail: 'Collect source files, URLs, text, and generation preferences.' },
+ { id: 'research', label: 'Research', detail: 'Ground claims with uploaded content, Google Search, and URL context when needed.' },
+ { id: 'plan', label: 'Plan', detail: 'Finalize text, facts, layout hierarchy, palette, and image prompt.' },
+ { id: 'render', label: 'Render', detail: 'Send the verified prompt and references to the image model.' },
+ { id: 'review', label: 'Review', detail: 'Hold the current artifact for human approval or targeted feedback.' },
+ { id: 'refine', label: 'Refine', detail: 'Apply one requested edit while preserving unchanged content.' },
+];
+
+function createAgentLoopState({
+ sessionId = `loop_${Date.now()}`,
+ turn = 0,
+ activePhase = 'intake',
+ goal = 'Create an infographic from user-provided context.',
+ stopRule = 'Stop after a rendered draft; continue only when the user asks for a refinement.',
+ hitlStatus,
+ interactionId,
+ previousInteractionId,
+}: {
+ sessionId?: string;
+ turn?: number;
+ activePhase?: AgentLoopPhaseId;
+ goal?: string;
+ stopRule?: string;
+ hitlStatus?: AgentLoopState['hitlStatus'];
+ interactionId?: string;
+ previousInteractionId?: string;
+} = {}): AgentLoopState {
+ const activeIndex = LOOP_PHASES.findIndex(phase => phase.id === activePhase);
+ const phases = LOOP_PHASES.map((phase, index) => {
+ let status: AgentLoopState['phases'][number]['status'] = 'pending';
+ if (activePhase === 'refine') {
+ status = phase.id === 'refine' ? 'active' : 'complete';
+ } else if (activeIndex >= 0) {
+ if (index < activeIndex) status = 'complete';
+ if (index === activeIndex) status = 'active';
+ }
+ return { ...phase, status };
+ });
+
+ return {
+ sessionId,
+ turn,
+ goal,
+ stopRule,
+ stateBackend: 'browser-local',
+ hitlStatus: hitlStatus ?? (activePhase === 'review' ? 'awaiting-input' : 'running'),
+ interactionId,
+ previousInteractionId,
+ phases,
+ };
+}
+
+function buildLoopGoal(config: InfographicConfig, filesCount: number): string {
+ return `Create a ${config.mode} infographic from ${filesCount} source ${filesCount === 1 ? 'item' : 'items'}.`;
+}
+
+function normalizeResolution(value: unknown): ImageResolution {
+ return SUPPORTED_RESOLUTIONS.includes(value as ImageResolution)
+ ? value as ImageResolution
+ : DEFAULT_INFOGRAPHIC_CONFIG.resolution;
+}
function loadHasVisited(): boolean {
try {
@@ -40,10 +104,6 @@ function safePersist(key: string, value: string): void {
}
}
-const isMasterView = typeof window !== 'undefined' && new URLSearchParams(window.location.search).get('view') === 'master';
-const isProdDeploy = import.meta.env.PROD || import.meta.env.VITE_PRODUCTION_DEPLOY === 'true';
-const allowQualityModel = isMasterView && !isProdDeploy;
-
function loadAdminConfig(): AdminConfig {
try {
const stored = localStorage.getItem(ADMIN_STORAGE_KEY);
@@ -51,14 +111,9 @@ function loadAdminConfig(): AdminConfig {
const parsed = JSON.parse(stored);
// Enforce gemini-3.5-flash as the default and only supported analysis model
parsed.orchestratorModel = 'gemini-3.5-flash';
- // Enforce gemini-3.1-flash-lite-image if quality model is not allowed
- if (!allowQualityModel) {
- parsed.imageGenModel = 'gemini-3.1-flash-lite-image';
- } else {
- if (parsed.imageGenModel === 'gemini-3.1-flash-image-preview') {
- parsed.imageGenModel = 'gemini-3.1-flash-image';
- }
- }
+ // Enforce gemini-3.1-flash-lite-image as the only supported image model
+ parsed.imageGenModel = 'gemini-3.1-flash-lite-image';
+ parsed.imageResolution = normalizeResolution(parsed.imageResolution);
return { ...DEFAULT_ADMIN_CONFIG, ...parsed };
}
} catch { /* ignore */ }
@@ -76,7 +131,11 @@ function loadTheme(): 'light' | 'dark' {
function loadUserConfig(): InfographicConfig {
try {
const stored = localStorage.getItem(CONFIG_STORAGE_KEY);
- if (stored) return { ...DEFAULT_INFOGRAPHIC_CONFIG, ...JSON.parse(stored) };
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ parsed.resolution = normalizeResolution(parsed.resolution);
+ return { ...DEFAULT_INFOGRAPHIC_CONFIG, ...parsed };
+ }
} catch { /* ignore */ }
return { ...DEFAULT_INFOGRAPHIC_CONFIG };
}
@@ -105,6 +164,7 @@ const initialState: AppState = {
chatMessages: [],
refineThoughts: [],
isProcessingFiles: false,
+ agentLoop: createAgentLoopState({ hitlStatus: 'not-started' }),
};
export function useInfographicFlow() {
@@ -203,7 +263,23 @@ export function useInfographicFlow() {
}, []);
const handleGenerate = useCallback(async () => {
- setState(s => ({ ...s, step: 'studio', generationPhase: 'preparing', error: null, streamingText: '', thoughtBubbles: [], refineThoughts: [], chatMessages: [] }));
+ const sessionId = `loop_${Date.now()}`;
+ setState(s => ({
+ ...s,
+ step: 'studio',
+ generationPhase: 'preparing',
+ error: null,
+ streamingText: '',
+ thoughtBubbles: [],
+ refineThoughts: [],
+ chatMessages: [],
+ agentLoop: createAgentLoopState({
+ sessionId,
+ turn: 1,
+ activePhase: 'research',
+ goal: buildLoopGoal(state.config, state.files.length),
+ }),
+ }));
try {
localStorage.setItem(HAS_VISITED_KEY, 'true');
@@ -225,7 +301,18 @@ export function useInfographicFlow() {
};
const prepResult = await prepareInfographic(state.files, state.config, state.adminConfig, onThought);
- setState(s => ({ ...s, prepareResult: prepResult, streamingText: 'Preparation complete. Generating image...' }));
+ setState(s => ({
+ ...s,
+ prepareResult: prepResult,
+ streamingText: 'Preparation complete. Generating image...',
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'render',
+ goal: s.agentLoop.goal,
+ stopRule: s.agentLoop.stopRule,
+ }),
+ }));
// Phase 2: Generate image
setState(s => ({ ...s, generationPhase: 'generating', streamingText: 'Generating infographic...' }));
@@ -265,6 +352,14 @@ export function useInfographicFlow() {
streamingText: '',
history: [historyEntry, ...s.history].slice(0, 20),
chatMessages: [firstMessage],
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'review',
+ goal: s.agentLoop.goal,
+ stopRule: 'Stop here unless the user requests a focused refinement.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
} catch (err) {
setState(s => ({
@@ -273,6 +368,14 @@ export function useInfographicFlow() {
step: 'create',
generationPhase: 'idle',
streamingText: '',
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'intake',
+ goal: s.agentLoop.goal,
+ stopRule: 'Resolve the error or adjust source inputs before restarting.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
}
}, [state.files, state.config, state.adminConfig]);
@@ -286,7 +389,20 @@ export function useInfographicFlow() {
text: instruction,
timestamp: Date.now(),
};
- setState(s => ({ ...s, error: null, streamingText: 'Refining infographic...', chatMessages: [...s.chatMessages, userMsg], refineThoughts: [] }));
+ setState(s => ({
+ ...s,
+ error: null,
+ streamingText: 'Refining infographic...',
+ chatMessages: [...s.chatMessages, userMsg],
+ refineThoughts: [],
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn + 1,
+ activePhase: 'refine',
+ goal: `Apply focused edit: ${instruction.slice(0, 80)}`,
+ stopRule: 'Stop after this edit is rendered and reviewed.',
+ }),
+ }));
try {
const onRefineThought = (thought: string) => {
@@ -368,12 +484,28 @@ export function useInfographicFlow() {
history: [historyEntry, ...s.history].slice(0, 20),
chatMessages: [...s.chatMessages, assistantMsg],
refineThoughts: [],
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'review',
+ goal: s.agentLoop.goal,
+ stopRule: 'Stop here unless the user requests another focused refinement.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
} catch (err) {
setState(s => ({
...s,
error: (err as Error).message,
streamingText: '',
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'review',
+ goal: s.agentLoop.goal,
+ stopRule: 'Resolve the failed refinement or request a narrower edit.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
}
}, [state.currentResult, state.adminConfig, state.config, state.chatMessages, state.prepareResult]);
@@ -393,7 +525,20 @@ export function useInfographicFlow() {
text: `Upgrade resolution to ${res}`,
timestamp: Date.now(),
};
- setState(s => ({ ...s, error: null, streamingText: `Upgrading to ${res} resolution...`, chatMessages: [...s.chatMessages, userMsg], refineThoughts: [] }));
+ setState(s => ({
+ ...s,
+ error: null,
+ streamingText: `Upgrading to ${res} resolution...`,
+ chatMessages: [...s.chatMessages, userMsg],
+ refineThoughts: [],
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn + 1,
+ activePhase: 'refine',
+ goal: `Upgrade the current infographic to ${res} resolution.`,
+ stopRule: 'Stop after the higher-resolution render is available for review.',
+ }),
+ }));
try {
const onRefineThought = (thought: string) => {
@@ -473,12 +618,28 @@ export function useInfographicFlow() {
history: [historyEntry, ...s.history].slice(0, 20),
chatMessages: [...s.chatMessages, assistantMsg],
refineThoughts: [],
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'review',
+ goal: s.agentLoop.goal,
+ stopRule: 'Stop here unless the user requests another focused refinement.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
} catch (err) {
setState(s => ({
...s,
error: (err as Error).message,
streamingText: '',
+ agentLoop: createAgentLoopState({
+ sessionId: s.agentLoop.sessionId,
+ turn: s.agentLoop.turn,
+ activePhase: 'review',
+ goal: s.agentLoop.goal,
+ stopRule: 'Resolve the failed resolution upgrade or request a lower resolution.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
}
}, [state.currentResult, state.adminConfig, state.config, updateAdminConfig, updateConfig, state.chatMessages, state.prepareResult]);
@@ -499,9 +660,17 @@ export function useInfographicFlow() {
setState(s => ({
...s,
currentResult: result,
- config: entry.config,
+ config: { ...entry.config, resolution: normalizeResolution(entry.config.resolution) },
step: 'studio',
generationPhase: 'complete',
+ agentLoop: createAgentLoopState({
+ sessionId: `history_${entry.id}`,
+ turn: 1,
+ activePhase: 'review',
+ goal: `Review saved infographic: ${entry.title}`,
+ stopRule: 'Stop here unless the user requests a focused refinement.',
+ hitlStatus: 'awaiting-input',
+ }),
}));
}, []);
@@ -575,6 +744,7 @@ Source: Global AI Index 2026 Report`;
thoughtBubbles: [],
chatMessages: [],
refineThoughts: [],
+ agentLoop: createAgentLoopState({ hitlStatus: 'not-started' }),
}));
}, []);
diff --git a/src/services/geminiService.ts b/src/services/geminiService.ts
index e09b994..63292b5 100644
--- a/src/services/geminiService.ts
+++ b/src/services/geminiService.ts
@@ -505,7 +505,6 @@ function mapResolutionToApi(res: string, model: string): string {
return '1K';
}
if (res === '0.5K') return '1K'; // '512' is not supported by Gemini image generation models
- if (res === '3K') return '2K'; // Fallback for unsupported 3K
return res;
}
diff --git a/src/types.ts b/src/types.ts
index c757527..0c08101 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,6 +1,9 @@
// === Step Types ===
export type StepType = 'hero' | 'create' | 'studio';
export type GenerationPhase = 'idle' | 'preparing' | 'generating' | 'complete';
+export type AgentLoopPhaseId = 'intake' | 'research' | 'plan' | 'render' | 'review' | 'refine';
+export type AgentLoopPhaseStatus = 'pending' | 'active' | 'complete';
+export type AgentLoopStateBackend = 'browser-local' | 'enterprise-interactions-ready';
// === Infographic Mode Types ===
export type InfographicMode =
@@ -11,7 +14,7 @@ export type InfographicMode =
| 'quick-slide'
| 'custom';
export type AspectRatio = '1:1' | '9:16' | '16:9' | '3:4' | '4:3' | '1:4';
-export type ImageResolution = '0.5K' | '1K' | '2K' | '3K' | '4K';
+export type ImageResolution = '0.5K' | '1K' | '2K';
// === File Types ===
export type FileCategory = 'document' | 'spreadsheet' | 'image' | 'text';
@@ -62,6 +65,26 @@ export interface ChatMessage {
timestamp: number;
}
+// === Agent Loop State ===
+export interface AgentLoopPhase {
+ id: AgentLoopPhaseId;
+ label: string;
+ status: AgentLoopPhaseStatus;
+ detail: string;
+}
+
+export interface AgentLoopState {
+ sessionId: string;
+ turn: number;
+ goal: string;
+ stopRule: string;
+ stateBackend: AgentLoopStateBackend;
+ hitlStatus: 'awaiting-input' | 'running' | 'not-started';
+ interactionId?: string;
+ previousInteractionId?: string;
+ phases: AgentLoopPhase[];
+}
+
// === Configuration ===
export interface AdminConfig {
geminiApiKey: string;
@@ -118,6 +141,7 @@ export interface AppState {
chatMessages: ChatMessage[];
refineThoughts: ThoughtBubble[];
isProcessingFiles: boolean;
+ agentLoop: AgentLoopState;
}
export interface HistoryEntry {