Skip to content

Implementation Guide: Single Message Processing Workflow

Component: ApplicationPage.tsx - Processing Step Date: 2025-10-01 Estimated Implementation Time: 4-6 hours Complexity: Medium


Quick Start Summary

What You're Building: Replace the stacked message cards (lines 443-475) with a single dynamic message container that smoothly transitions between agent states.

Key Changes: 1. Replace agentMessages array state with single currentMessage state 2. Update useEffect to orchestrate message sequence with fade transitions 3. Add CSS animations for fade-in/fade-out 4. Implement ARIA live region for accessibility


Step-by-Step Implementation

Step 1: Update State Management (5 minutes)

Location: /workspaces/loan-defenders/loan_defenders/ui/src/pages/application/ApplicationPage.tsx

Current Code (lines 18):

const [agentMessages, setAgentMessages] = useState<Array<{agent: string, message: string, icon: string}>>([]);

Replace With:

// Remove agentMessages state, add these instead:
const [currentMessage, setCurrentMessage] = useState<{
  agentName: string;
  icon: string;
  text: string;
  state: 'PROCESSING' | 'COMPLETE' | 'TRANSITION';
  progress: number;
}>({
  agentName: '',
  icon: '',
  text: '',
  state: 'PROCESSING',
  progress: 0
});

const [animationClass, setAnimationClass] = useState<string>('');


Step 2: Create Message Sequence Configuration (10 minutes)

Add this constant BEFORE the component (around line 10):

interface MessageSequence {
  id: string;
  agentName: string;
  icon: string;
  processingText: string;
  completeText: string;
  transitionText?: string;
  progressStart: number;
  progressEnd: number;
  timing: {
    processingDelay: number;
    completeDuration: number;
    transitionDuration: number;
  };
}

const MESSAGE_SEQUENCE: MessageSequence[] = [
  {
    id: 'intake',
    agentName: 'Cap-ital America',
    icon: '🦸‍♂️',
    processingText: 'Cap-ital America is reviewing your application...',
    completeText: '✅ Application validated! All information looks great.',
    transitionText: '🤝 Handing over to Sarah for credit analysis...',
    progressStart: 0,
    progressEnd: 25,
    timing: {
      processingDelay: 0,
      completeDuration: 500,
      transitionDuration: 500
    }
  },
  {
    id: 'credit',
    agentName: 'Sarah (Credit Analyst)',
    icon: '🦸‍♀️',
    processingText: 'Sarah is analyzing your credit profile...',
    completeText: '✅ Credit profile looks strong! Good payment history detected.',
    transitionText: '🤝 Handing over to Marcus for income verification...',
    progressStart: 25,
    progressEnd: 50,
    timing: {
      processingDelay: 1000,
      completeDuration: 500,
      transitionDuration: 500
    }
  },
  {
    id: 'income',
    agentName: 'Marcus (Income Verifier)',
    icon: '🦸',
    processingText: 'Marcus is verifying your income and employment...',
    completeText: '✅ Income verified! Debt-to-income ratio is healthy.',
    transitionText: '🤝 Handing over to Alex for risk assessment...',
    progressStart: 50,
    progressEnd: 75,
    timing: {
      processingDelay: 3000,
      completeDuration: 500,
      transitionDuration: 500
    }
  },
  {
    id: 'risk',
    agentName: 'Alex (Risk Assessor)',
    icon: '🦹‍♂️',
    processingText: 'Alex is performing final risk assessment...',
    completeText: '✅ Risk assessment complete! All metrics look excellent.\n🎉 Your application has been APPROVED!',
    transitionText: undefined,
    progressStart: 75,
    progressEnd: 100,
    timing: {
      processingDelay: 5000,
      completeDuration: 2000,
      transitionDuration: 0
    }
  }
];

Step 3: Replace useEffect Logic (20 minutes)

Current Code (lines 54-102):

useEffect(() => {
  if (currentStep !== 'processing') {
    return;
  }

  const stages = [
    // ... existing stages array ...
  ];

  const timers = stages.map(stage =>
    setTimeout(() => {
      setProcessingProgress(stage.progress);
      setAgentMessages(prev => [...prev, {
        agent: stage.agent,
        message: stage.message,
        icon: stage.icon
      }]);
    }, stage.delay)
  );

  return () => timers.forEach(clearTimeout);
}, [currentStep]);

Replace With:

useEffect(() => {
  if (currentStep !== 'processing') {
    return;
  }

  const timers: NodeJS.Timeout[] = [];

  MESSAGE_SEQUENCE.forEach((sequence, index) => {
    // 1. Show COMPLETE state immediately (for first agent) or after processing
    const completeDelay = sequence.timing.processingDelay;

    timers.push(setTimeout(() => {
      setCurrentMessage({
        agentName: sequence.agentName,
        icon: sequence.icon,
        text: sequence.completeText,
        state: 'COMPLETE',
        progress: sequence.progressEnd
      });
      setProcessingProgress(sequence.progressEnd);
    }, completeDelay));

    // 2. Show TRANSITION state (if not last agent)
    if (sequence.transitionText && index < MESSAGE_SEQUENCE.length - 1) {
      const transitionDelay = completeDelay + sequence.timing.completeDuration;

      timers.push(setTimeout(() => {
        setCurrentMessage({
          agentName: sequence.agentName,
          icon: '🤝',
          text: sequence.transitionText!,
          state: 'TRANSITION',
          progress: sequence.progressEnd
        });
      }, transitionDelay));

      // 3. Trigger fade-out animation before next agent
      const fadeOutDelay = transitionDelay + sequence.timing.transitionDuration - 200;

      timers.push(setTimeout(() => {
        setAnimationClass('message-fade-out');
      }, fadeOutDelay));

      // 4. Show next agent's PROCESSING state with fade-in
      const nextSequence = MESSAGE_SEQUENCE[index + 1];
      const nextProcessingDelay = transitionDelay + sequence.timing.transitionDuration;

      timers.push(setTimeout(() => {
        setAnimationClass('message-fade-in');
        setCurrentMessage({
          agentName: nextSequence.agentName,
          icon: nextSequence.icon,
          text: nextSequence.processingText,
          state: 'PROCESSING',
          progress: nextSequence.progressStart
        });

        // Remove animation class after animation completes
        setTimeout(() => setAnimationClass(''), 300);
      }, nextProcessingDelay));
    }
  });

  return () => timers.forEach(clearTimeout);
}, [currentStep]);


Step 4: Add CSS Animations (5 minutes)

Location: /workspaces/loan-defenders/loan_defenders/ui/src/index.css or /workspaces/loan-defenders/loan_defenders/ui/src/styles/global.css

Add these styles:

/* Fade Out Animation */
@keyframes fadeOut {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-10px);
  }
}

.message-fade-out {
  animation: fadeOut 200ms ease-out forwards;
}

/* Fade In Animation */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message-fade-in {
  animation: fadeIn 300ms ease-in forwards;
}

/* Pulse Animation for Processing State */
@keyframes pulse-soft {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.7;
  }
}

.message-pulse {
  animation: pulse-soft 2s ease-in-out infinite;
}

/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  .message-fade-out,
  .message-fade-in,
  .message-pulse {
    animation: none;
    transition: none;
  }
}

Step 5: Update JSX (15 minutes)

Current Code (lines 443-475):

{/* Agent Messages Ticker */}
{agentMessages.length > 0 && (
  <div className="mt-10 space-y-3">
    <h3 className="text-lg font-semibold text-center mb-4">
      <span className="bg-gradient-to-r from-brand-600 via-accent-600 to-brand-500 bg-clip-text text-transparent">
        💬 What Your AI Team Says
      </span>
    </h3>
    <div className="space-y-3 max-h-64 overflow-y-auto">
      {agentMessages.map((msg, idx) => (
        <div key={idx} className="bg-gradient-to-r from-gray-50 to-blue-50 rounded-lg p-4 border animate-fade-in-up">
          {/* ... message content ... */}
        </div>
      ))}
    </div>
  </div>
)}

Replace With:

{/* Single Agent Message Container */}
{currentMessage.agentName && (
  <div className="mt-10">
    <h3 className="text-lg font-semibold text-center mb-4">
      <span className="bg-gradient-to-r from-brand-600 via-accent-600 to-brand-500 bg-clip-text text-transparent">
        💬 What Your AI Team Says
      </span>
    </h3>

    {/* Single Message Container with ARIA live region */}
    <div
      role="status"
      aria-live="polite"
      aria-atomic="true"
      aria-label={`${currentMessage.agentName} says: ${currentMessage.text}. Progress: ${currentMessage.progress} percent complete.`}
      className={`
        bg-gradient-to-r from-gray-50 to-blue-50
        dark:from-dark-bg-tertiary dark:to-dark-bg-card
        rounded-lg p-6 border border-gray-200
        dark:border-gray-700 shadow-md
        min-h-[120px] flex items-center
        transition-all duration-300
        ${animationClass}
      `}
    >
      <div className="flex items-start space-x-4 w-full">
        {/* Agent Icon */}
        <div className="w-12 h-12 bg-gradient-to-br from-brand-500 to-brand-600 rounded-full flex items-center justify-center flex-shrink-0">
          <span className="text-2xl" aria-hidden="true">
            {currentMessage.icon}
          </span>
        </div>

        {/* Message Content */}
        <div className="flex-1">
          <p className="font-semibold text-gray-900 dark:text-dark-text-primary text-sm mb-2">
            {currentMessage.agentName}
          </p>
          <p className="text-gray-700 dark:text-dark-text-secondary text-base leading-relaxed whitespace-pre-line">
            {currentMessage.text}
          </p>
        </div>

        {/* Processing Indicator (show only during PROCESSING state) */}
        {currentMessage.state === 'PROCESSING' && (
          <div className="flex space-x-1 items-center" aria-hidden="true">
            <div className="w-2 h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
            <div className="w-2 h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
            <div className="w-2 h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
          </div>
        )}
      </div>
    </div>
  </div>
)}


Step 6: Mobile Responsiveness (5 minutes)

Update the container classes to be responsive:

<div
  className={`
    bg-gradient-to-r from-gray-50 to-blue-50
    dark:from-dark-bg-tertiary dark:to-dark-bg-card
    rounded-lg p-4 sm:p-6 border border-gray-200
    dark:border-gray-700 shadow-md
    min-h-[100px] sm:min-h-[120px]
    flex items-center
    transition-all duration-300
    ${animationClass}
  `}
>
  <div className="flex items-start space-x-3 sm:space-x-4 w-full">
    {/* Agent Icon - responsive sizing */}
    <div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-brand-500 to-brand-600 rounded-full flex items-center justify-center flex-shrink-0">
      <span className="text-xl sm:text-2xl" aria-hidden="true">
        {currentMessage.icon}
      </span>
    </div>

    {/* Message Content - responsive text sizing */}
    <div className="flex-1">
      <p className="font-semibold text-gray-900 dark:text-dark-text-primary text-xs sm:text-sm mb-1 sm:mb-2">
        {currentMessage.agentName}
      </p>
      <p className="text-gray-700 dark:text-dark-text-secondary text-sm sm:text-base leading-relaxed whitespace-pre-line">
        {currentMessage.text}
      </p>
    </div>

    {/* Processing Indicator */}
    {currentMessage.state === 'PROCESSING' && (
      <div className="flex space-x-1 items-center" aria-hidden="true">
        <div className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }}></div>
        <div className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }}></div>
        <div className="w-1.5 h-1.5 sm:w-2 sm:h-2 bg-brand-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }}></div>
      </div>
    )}
  </div>
</div>

Testing Checklist

Functional Testing

Before Committing: - [ ] Run uv run ruff check . --fix (auto-fix linting issues) - [ ] Run uv run ruff format . (auto-format code) - [ ] Run frontend build: cd loan_defenders/ui && npm run build - [ ] Test in development: npm run dev

Visual Testing

Test these scenarios: - [ ] Message transitions smoothly from one agent to next - [ ] Fade-out animation completes before fade-in starts - [ ] Processing dots animate correctly - [ ] No layout shift during transitions - [ ] Dark mode renders properly - [ ] Mobile view (test at 375px width) - [ ] Tablet view (test at 768px width) - [ ] Desktop view (test at 1440px width)

Timing Validation

Use browser DevTools:

// In browser console, test timing:
console.time('workflow');
// Navigate to processing page
// When 100% complete:
console.timeEnd('workflow');
// Should show ~6 seconds

Expected Timeline: - 0.0s: Cap-ital America complete - 0.5s: Handoff to Sarah - 1.0s: Sarah processing - 2.0s: Sarah complete - 2.5s: Handoff to Marcus - 3.0s: Marcus processing - 4.0s: Marcus complete - 4.5s: Handoff to Alex - 5.0s: Alex processing - 6.0s: Alex complete (100%)

Accessibility Testing

Manual Tests: - [ ] Turn on screen reader (macOS VoiceOver: Cmd+F5) - [ ] Navigate to processing page - [ ] Verify screen reader announces each message update - [ ] Check that ARIA label includes full context - [ ] Test with keyboard navigation (Tab key) - [ ] Verify reduced motion preference respected

Automated Tests:

# Install axe-core if not already installed
npm install --save-dev @axe-core/cli

# Run accessibility audit
npx axe http://localhost:5173/application --include '.single-message-container'

Performance Testing

Chrome DevTools Performance: 1. Open DevTools → Performance tab 2. Click Record 3. Navigate to processing page 4. Let workflow complete 5. Stop recording 6. Check for: - Frame rate stays above 60fps - No long tasks >50ms - No layout thrashing

Expected Performance: - Initial render: <100ms - Animation frame time: <16ms (60fps) - Memory usage: No leaks (test by repeating workflow 10x)


Common Issues & Solutions

Issue 1: Animations Don't Play

Symptoms: Messages update instantly without fade transitions

Solution: Check that CSS animations are loaded

// Verify animation class is applied
console.log('Animation class:', animationClass);

// Check if CSS is loaded in browser DevTools → Elements → Computed

Fix: Ensure index.css or global.css is imported in your component or main entry point


Issue 2: Timing Feels Off

Symptoms: Transitions too fast or too slow

Solution: Adjust timing constants in MESSAGE_SEQUENCE

// Too fast? Increase delays:
timing: {
  processingDelay: 1500,  // Was 1000
  completeDuration: 750,  // Was 500
  transitionDuration: 750 // Was 500
}

// Too slow? Decrease delays:
timing: {
  processingDelay: 750,   // Was 1000
  completeDuration: 300,  // Was 500
  transitionDuration: 300 // Was 500
}

Issue 3: Screen Reader Announces Too Much

Symptoms: Screen reader reads multiple messages in rapid succession

Solution: Verify aria-live="polite" (NOT "assertive")

<div
  role="status"
  aria-live="polite"  //  Correct - waits for user to be idle
  // NOT aria-live="assertive"  - interrupts immediately
  aria-atomic="true"
>

Issue 4: Layout Shifts on Mobile

Symptoms: Content jumps when message updates

Solution: Set minimum height on container

<div className="min-h-[100px] sm:min-h-[120px]">
  {/* Prevents layout shift */}
</div>

Issue 5: Dark Mode Colors Look Wrong

Symptoms: Poor contrast in dark mode

Solution: Test both modes and adjust colors

<div className="
  bg-gradient-to-r from-gray-50 to-blue-50
  dark:from-dark-bg-tertiary dark:to-dark-bg-card
  text-gray-700 dark:text-dark-text-secondary
">

Verify with DevTools: - Toggle dark mode in browser - Check contrast ratios (should be >4.5:1 for text)


Code Review Checklist

Before requesting code review: - [ ] All pre-commit checks pass (ruff, tests, build) - [ ] TypeScript types are correct (no any types) - [ ] CSS animations work in all browsers (Chrome, Firefox, Safari) - [ ] Mobile responsiveness tested on real device or emulator - [ ] Accessibility attributes present (role, aria-live, aria-label) - [ ] No console errors or warnings - [ ] Code follows existing patterns in codebase - [ ] Comments added for complex logic - [ ] No hardcoded values (use constants)


Deployment Steps

1. Local Testing

# Frontend
cd loan_defenders/ui
npm run dev
# Test at http://localhost:5173/application

# Backend (if needed)
cd ../..
uv run python -m loan_defenders.api.app

2. Pre-Deployment Checks

# Run all quality checks
uv run ruff check . --fix
uv run ruff format .
cd loan_defenders/ui && npm run build
cd ../.. && uv run pytest tests/ -v

# Verify no TypeScript errors
cd loan_defenders/ui && npm run type-check

3. Create Pull Request

git add .
git commit -m "feat: implement single message processing workflow

- Replace stacked message cards with single dynamic container
- Add smooth fade transitions between agent messages
- Include handoff messaging for workflow clarity
- Implement ARIA live region for accessibility
- Add mobile-responsive design

Closes #[issue-number]"

git push origin feat/single-message-workflow

4. PR Description Template

## Summary
Implements single message processing workflow to improve user experience during loan application processing.

## Changes
- Replaced stacked message cards with single dynamic container
- Added fade-in/fade-out transitions between agent messages
- Implemented handoff messaging ("Handing over to Sarah...")
- Added ARIA live region for screen reader accessibility
- Made container mobile-responsive

## Testing
- [x] Visual testing on Chrome, Firefox, Safari
- [x] Mobile testing on iPhone, Android
- [x] Screen reader testing with VoiceOver
- [x] Performance profiling (60fps maintained)
- [x] Dark mode compatibility verified

## Screenshots
[Add before/after screenshots]

## Related Documentation
- UX Design: `/docs/ux/processing-workflow-single-message-design.md`
- User Journey: `/docs/ux/processing-workflow-user-journey.md`

Closes #[issue-number]

Post-Deployment Monitoring

Analytics to Track

  • Average workflow completion time: Should be ~6 seconds
  • User abandonment rate: Should decrease from current baseline
  • Mobile vs Desktop usage: Track experience differences
  • Accessibility tool usage: Monitor screen reader interactions

User Feedback Collection

  • Add in-app survey after processing completes
  • Question: "How clear was the processing workflow?" (1-5 scale)
  • Optional comment: "Any suggestions for improvement?"

A/B Testing (Optional)

  • Group A: New single message design (50% of users)
  • Group B: Old stacked messages design (50% of users)
  • Duration: 2 weeks
  • Primary Metric: User satisfaction rating
  • Secondary Metrics: Abandonment rate, time to results view

Modified Files: - /workspaces/loan-defenders/loan_defenders/ui/src/pages/application/ApplicationPage.tsx (lines 18, 54-102, 443-475) - /workspaces/loan-defenders/loan_defenders/ui/src/index.css (add animations)

Reference Documentation: - UX Design Spec: /workspaces/loan-defenders/docs/ux/processing-workflow-single-message-design.md - User Journey: /workspaces/loan-defenders/docs/ux/processing-workflow-user-journey.md

Supporting Tools: - Tailwind CSS: https://tailwindcss.com/docs - React Hooks: https://react.dev/reference/react - ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/


Support & Questions

Need Help?: - Review UX design spec for design rationale - Check user journey doc for user experience context - Consult code-reviewer agent for implementation feedback - Ask ux-ui-designer agent for design clarifications

Common Questions:

Q: Can I adjust the timing? A: Yes! Update MESSAGE_SEQUENCE timing values. Recommended to keep total workflow between 5-8 seconds.

Q: Can I add more agents? A: Yes! Add new entries to MESSAGE_SEQUENCE array. Adjust timing to keep workflow under 10 seconds total.

Q: What if animations cause performance issues? A: Use GPU-accelerated properties (transform, opacity). Test with Chrome DevTools Performance profiler.

Q: How do I test accessibility? A: Use screen reader (VoiceOver on macOS, NVDA on Windows), run axe-core automated tests, verify ARIA attributes.


Implementation Status: Ready for development Next Step: Create GitHub issue and assign to developer