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
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
Related Files
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