Skip to content

Instantly share code, notes, and snippets.

@anton-pt
Created June 6, 2025 08:22
Show Gist options
  • Select an option

  • Save anton-pt/032a2f5c7889602e1a77a6b2b5d45a0a to your computer and use it in GitHub Desktop.

Select an option

Save anton-pt/032a2f5c7889602e1a77a6b2b5d45a0a to your computer and use it in GitHub Desktop.
Example user story for AI coding assistant

Story BRIEF-V2-004: Add Short Bio Interim Flow

Overview

Priority: High | Effort: 4 hours | Dependencies: BRIEF-V2-001, BRIEF-V2-002

Add support for the new shortbio-final message from the backend and implement an engaging interim flow that displays key profile insights while users wait for the full briefing to complete.


Acceptance Criteria

βœ… Core Requirements

  • Add TypeScript interface for shortbio-final message structure
  • Update useBriefingSearchV2 hook to handle new message type
  • Add timing logic: show short bio cards only after 10 seconds AND when message arrives
  • Implement card sequence: headline β†’ uniqueQualifier β†’ connectionValue β†’ achievements β†’ personalNote
  • Each card displays for 7-10 seconds with CircularTimer component
  • Chevron appears after 7 seconds, auto-advance at 10 seconds
  • Smooth transitions between cards with proper loading states
  • Cards use ContentCard component with ContentLabelChip for categories

βœ… User Experience Requirements

  • Profile confirmation shows for exactly 10 seconds before short bio cards begin
  • Cards auto-advance smoothly without jarring transitions
  • User can manually advance cards using chevron after 7 seconds
  • Clear visual progression through the card sequence
  • Graceful handling if shortbio-final message arrives before or after 10 seconds
  • Cards stop and transition to full briefing when formatted-final message arrives

βœ… Quality Requirements

  • No console errors during short bio flow
  • Proper cleanup of timers when component unmounts or flow changes
  • TypeScript strict mode compliance for all new interfaces
  • Responsive design works on mobile and desktop
  • Accessibility support for card navigation

Technical Implementation

Implementation Steps

Step 1: Add TypeScript Interfaces

// Add to useBriefingSearchV2.ts after existing message interfaces:

interface ShortBioFinalMessage {
  kind: 'shortbio-final';
  data: {
    fullBio: string;
    headline: string;
    metadata: {
      confidence: number;
      dataCompleteness: number;
      profileChallenges: string[];
    };
    currentRole: {
      title: string;
      duration: string;
      organization: string;
    };
    achievements: string[];
    personalNote: string;
    profileQuality: 'complete' | 'limited';
    connectionValue: string;
    uniqueQualifier: string;
  };
}

// Update the ResponseMessage union type:
type ResponseMessage =
  | ProfileMessage
  | BriefingIncrementalMessage
  | BriefingFinalMessage
  | FormattedFinalMessage
  | ShortBioFinalMessage  // Add this line
  | ErrorMessage;

// Define card data structure:
interface ShortBioCard {
  id: string;
  label: string;
  content: string;
  duration: number; // seconds
}

Step 2: Add New State Variables to Hook

// Add these state variables to useBriefingSearchV2:

const [shortBioData, setShortBioData] = useState<ShortBioFinalMessage['data'] | null>(null);
const [showShortBioCards, setShowShortBioCards] = useState(false);
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [profileConfirmationStartTime, setProfileConfirmationStartTime] = useState<number | null>(null);

// Add timer ref for cleanup:
const shortBioTimerRef = useRef<NodeJS.Timeout | null>(null);

Step 3: Update processMessage Function

// Update the processMessage function to handle shortbio-final:

const processMessage = (message: ResponseMessage) => {
  setTimeout(() => {
    switch (message.kind) {
      case 'profile':
        setProfileData(message.profile);
        setSearchStatus(SearchStatus.LOADING);
        setIsLoading(false);
        // Start timing for short bio cards
        setProfileConfirmationStartTime(Date.now());
        break;
        
      case 'shortbio-final':
        setShortBioData(message.data);
        // Check if 10 seconds have passed since profile confirmation
        checkShortBioDisplayConditions();
        break;
        
      case 'formatted-final': {
        // Stop short bio cards and show full briefing
        setShowShortBioCards(false);
        const sections = convertFormattedToSections(message.data);
        setBriefingSections(sections);
        setSearchStatus(SearchStatus.LOADED);
        break;
      }
      
      // ... existing cases remain unchanged
    }
  }, 0);
};

Step 4: Add Short Bio Display Logic

// Add these functions to the hook:

const checkShortBioDisplayConditions = useCallback(() => {
  if (!shortBioData || !profileConfirmationStartTime) return;
  
  const timeSinceConfirmation = Date.now() - profileConfirmationStartTime;
  const tenSecondsElapsed = timeSinceConfirmation >= 10000;
  
  if (tenSecondsElapsed) {
    setShowShortBioCards(true);
    setCurrentCardIndex(0);
  } else {
    // Wait for remaining time then show cards
    const remainingTime = 10000 - timeSinceConfirmation;
    shortBioTimerRef.current = setTimeout(() => {
      setShowShortBioCards(true);
      setCurrentCardIndex(0);
    }, remainingTime);
  }
}, [shortBioData, profileConfirmationStartTime]);

// Check conditions when shortBioData changes
useEffect(() => {
  if (shortBioData) {
    checkShortBioDisplayConditions();
  }
}, [shortBioData, checkShortBioDisplayConditions]);

// Also check when 10 seconds pass (for cases where message arrives early)
useEffect(() => {
  if (profileConfirmationStartTime && shortBioData) {
    const timer = setTimeout(() => {
      checkShortBioDisplayConditions();
    }, 10000);
    
    return () => clearTimeout(timer);
  }
}, [profileConfirmationStartTime, shortBioData, checkShortBioDisplayConditions]);

Step 5: Add Card Generation Function

// Add this function to generate card data:

const generateShortBioCards = useCallback((data: ShortBioFinalMessage['data']): ShortBioCard[] => {
  const cards: ShortBioCard[] = [
    {
      id: 'headline',
      label: 'Profile Overview',
      content: data.headline,
      duration: 8,
    },
    {
      id: 'unique-qualifier',
      label: 'What Makes Them Unique',
      content: data.uniqueQualifier,
      duration: 9,
    },
    {
      id: 'connection-value',
      label: 'Why Connect',
      content: data.connectionValue,
      duration: 8,
    },
  ];

  // Add achievement cards
  data.achievements.forEach((achievement, index) => {
    cards.push({
      id: `achievement-${index}`,
      label: `Key Achievement ${index + 1}`,
      content: achievement,
      duration: 7,
    });
  });

  // Add personal note card
  cards.push({
    id: 'personal-note',
    label: 'Personal Insight',
    content: data.personalNote,
    duration: 8,
  });

  return cards;
}, []);

Step 6: Add Card Navigation Functions

// Add these functions for card navigation:

const advanceToNextCard = useCallback(() => {
  if (!shortBioData) return;
  
  const cards = generateShortBioCards(shortBioData);
  const nextIndex = currentCardIndex + 1;
  
  if (nextIndex >= cards.length) {
    // Finished all cards, wait for full briefing or show loading
    setShowShortBioCards(false);
  } else {
    setCurrentCardIndex(nextIndex);
  }
}, [currentCardIndex, shortBioData, generateShortBioCards]);

const handleCardTimerComplete = useCallback(() => {
  advanceToNextCard();
}, [advanceToNextCard]);

Step 7: Update Hook Return Object

// Add new state and functions to the hook's return:

return {
  // ... all existing returns
  shortBioData,
  showShortBioCards,
  currentCardIndex,
  generateShortBioCards,
  handleCardTimerComplete,
};

Step 8: Update Cleanup in dismissSearch

// Update dismissSearch function to clean up short bio state:

const dismissSearch = useCallback(() => {
  setIsDismissing(true);
  
  // Cancel any ongoing streams
  if (streamReaderRef.current.reader) {
    streamReaderRef.current.reader.cancel();
  }
  if (streamReaderRef.current.controller) {
    streamReaderRef.current.controller.abort();
  }

  // Clean up short bio timers
  if (shortBioTimerRef.current) {
    clearTimeout(shortBioTimerRef.current);
    shortBioTimerRef.current = null;
  }

  // Reset all state including new short bio state
  setSearchStatus(SearchStatus.WAITING_FOR_INPUT);
  setCurrentStage(0);
  setProfileData({
    targetName: 'Loading Profile...',
    headline: 'Please wait while we fetch the profile information',
  });
  setBriefingSections([]);
  setDetailedBriefingNotes('');
  setSubmissionError(null);
  setIsLoading(false);
  setIsRefreshing(false);
  setSessionDetails(null);
  setLastDismissalReason('user_dismissed');
  
  // Reset short bio state
  setShortBioData(null);
  setShowShortBioCards(false);
  setCurrentCardIndex(0);
  setProfileConfirmationStartTime(null);
  
  // Clear dismissing state after a brief delay for UI feedback
  setTimeout(() => {
    setIsDismissing(false);
  }, 100);
}, []);

Step 9: Add Component Cleanup

// Add useEffect for component unmount cleanup:

useEffect(() => {
  return () => {
    if (shortBioTimerRef.current) {
      clearTimeout(shortBioTimerRef.current);
    }
  };
}, []);

UI Implementation in BriefingPageV2

Step 10: Add Short Bio Cards Rendering Function

// Add this function to BriefingPageV2 component:

const renderShortBioCards = () => {
  if (!showShortBioCards || !shortBioData) return null;
  
  const cards = generateShortBioCards(shortBioData);
  const currentCard = cards[currentCardIndex];
  
  if (!currentCard) return null;

  return (
    <div className="flex min-h-screen items-center justify-center px-4 bg-gradient-to-br from-cyan-50 to-cyan-100">
      <div className="w-full max-w-md">
        {/* Header with profile info */}
        <div className="text-center mb-8">
          <div className="flex items-center justify-center space-x-2 mb-2">
            <div className="w-6 h-6 bg-cyan-600 rounded-full flex items-center justify-center">
              <span className="text-white text-xs font-bold">πŸ“‹</span>
            </div>
            <span className="text-sm font-medium text-gray-600">
              Preparing your brief about {profileData.targetName}
            </span>
          </div>
        </div>

        {/* Current Card */}
        <ContentCard
          label={currentCard.label}
          content={currentCard.content}
          duration={currentCard.duration}
          onTimerComplete={handleCardTimerComplete}
        />

        {/* Progress Indicator */}
        <div className="mt-6 flex justify-center">
          <div className="flex space-x-2">
            {cards.map((_, index) => (
              <div
                key={index}
                className={`w-2 h-2 rounded-full transition-colors ${
                  index === currentCardIndex
                    ? 'bg-cyan-600'
                    : index < currentCardIndex
                    ? 'bg-cyan-400'
                    : 'bg-gray-300'
                }`}
              />
            ))}
          </div>
        </div>

        {/* Minimise Button */}
        <div className="text-center mt-8">
          <button
            onClick={dismissSearch}
            className="text-cyan-600 text-base font-normal underline hover:text-cyan-800 transition-colors"
          >
            Minimise
          </button>
        </div>
      </div>
    </div>
  );
};

Step 11: Update Main Component Render Logic

// Update the main component's return statement in BriefingPageV2:

return (
  <BriefingErrorBoundary>
    <div className="min-h-screen bg-white">
      {renderUrlInput()}
      {renderLoadingState()}
      {renderProfileConfirmation()}
      {renderShortBioCards()} {/* Add this line */}
      {renderBriefingContent()}
    </div>
  </BriefingErrorBoundary>
);

Step 12: Update Profile Confirmation Timing

// Update renderProfileConfirmation to auto-dismiss after 10 seconds:

const renderProfileConfirmation = () => {
  if (
    searchStatus !== SearchStatus.LOADING || 
    profileData.targetName === 'Loading Profile...' ||
    !profileData.targetName.trim() ||
    showShortBioCards // Don't show if short bio cards are active
  ) {
    return null;
  }

  return (
    <div className="flex min-h-screen items-center justify-center px-4">
      <ConfirmCard
        profileSrc={
          profileData.profilePicUrl || 
          `https://ui-avatars.com/api/?name=${encodeURIComponent(profileData.targetName)}&size=96&background=0891b2&color=fff`
        }
        name={profileData.targetName}
        description={profileData.headline || 'LinkedIn Professional'}
        buttonText="This isn't the right person"
        onConfirm={dismissSearch}
        duration={10} // Show for 10 seconds
        onTimerComplete={() => {
          // Timer complete - short bio cards should take over if ready
          if (shortBioData) {
            setShowShortBioCards(true);
            setCurrentCardIndex(0);
          }
        }}
      />
    </div>
  );
};

Testing Checklist

Unit Testing Requirements

  • Test shortbio-final message processing
  • Test 10-second timing logic works correctly
  • Test card generation from short bio data
  • Test card navigation and auto-advance
  • Test timer cleanup on component unmount
  • Test dismissal cleans up all short bio state

Integration Testing Requirements

  • Test complete flow from profile confirmation to short bio cards
  • Test transition from short bio cards to full briefing
  • Test early arrival of shortbio-final message (before 10 seconds)
  • Test late arrival of shortbio-final message (after 10 seconds)
  • Test dismissal during short bio card display
  • Test formatted-final message interrupting short bio flow

Manual Testing Requirements

  • Test complete user journey with short bio flow
  • Test card timing feels natural (7-10 seconds each)
  • Test chevron manual advance works after 7 seconds
  • Test progress indicator updates correctly
  • Test responsive design on mobile and desktop
  • Test dismissal works from any card in the sequence

Implementation Notes

Key Design Decisions

  1. Timing Control: 10-second delay ensures users have time to confirm profile
  2. Card Duration: Variable timing (7-10 seconds) keeps content engaging
  3. Manual Advancement: Chevron appears after 7 seconds for user control
  4. Progress Indication: Dots show user's progress through card sequence
  5. Graceful Interruption: Full briefing can interrupt short bio cards

State Flow

Profile Confirmation (10 seconds)
       ↓ (10 seconds elapsed + shortbio-final received)
Short Bio Cards (headline β†’ unique β†’ value β†’ achievements β†’ personal)
       ↓ (formatted-final message arrives)
Full Briefing Display

Message Timing Scenarios

Scenario 1: Normal Flow
- Profile arrives β†’ Start 10s timer
- shortbio-final arrives at 5s β†’ Wait for timer
- Timer completes at 10s β†’ Show cards

Scenario 2: Late Short Bio
- Profile arrives β†’ Start 10s timer  
- Timer completes at 10s β†’ Wait for shortbio-final
- shortbio-final arrives at 12s β†’ Show cards immediately

Scenario 3: Early Briefing
- Profile arrives β†’ Start 10s timer
- shortbio-final arrives at 5s β†’ Wait for timer
- Timer completes at 10s β†’ Show cards
- formatted-final arrives during cards β†’ Skip to briefing

Common Pitfalls to Avoid

  • Don't forget to clean up timers on component unmount
  • Don't show short bio cards if formatted-final has already arrived
  • Don't let profile confirmation timer interfere with card timers
  • Don't forget to handle edge cases (missing data, early completion)
  • Don't forget responsive design for mobile card display

This story creates an engaging interim experience that keeps users invested while maintaining smooth transitions to the full briefing experience!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment