Korai Docs
ChatWithVideo

Chat with Video - Hooks

Custom React hooks for video chat functionality

Overview

The Chat with Video feature uses custom hooks to encapsulate transcript fetching and thread generation logic. These hooks interact with the video chat store and provide clean interfaces for component usage.

useVideoTranscript Hook

Location: src/features/chat/hooks/use-video-transcript.ts

Purpose

Fetches video transcript from the /api/transcribe endpoint and updates the store with the transcript data.

Dependencies

  • useToast: For displaying success/error notifications
  • useVideoChatStore: For accessing and updating video state
  • extractVideoId: Utility to extract YouTube video ID from URL

Implementation

export const useVideoTranscript = () => {
  const { toast } = useToast();
  const {
    videoUrl,
    setVideoId,
    setTranscript,
    setHasTranscript,
    setIsLoadingTranscript
  } = useVideoChatStore();

  const fetchTranscript = useCallback(async () => {
    // Validation and fetching logic
  }, [videoUrl, toast, setVideoId, setTranscript, setHasTranscript, setIsLoadingTranscript]);

  return { fetchTranscript };
};

Validation Steps

1. Empty URL Check

if (!videoUrl.trim()) {
  toast({
    title: 'Error',
    description: 'Please enter a YouTube URL',
    variant: 'destructive'
  });
  return false;
}

Shows error toast if URL field is empty.

2. Video ID Extraction

const extractedVideoId = extractVideoId(videoUrl.trim());
if (!extractedVideoId) {
  toast({
    title: 'Error',
    description: 'Please enter a valid YouTube URL',
    variant: 'destructive'
  });
  return false;
}

Validates YouTube URL format and extracts video ID.

Fetch Process

1. Start Loading

setIsLoadingTranscript(true);
setVideoId(extractedVideoId);

Sets loading state and stores the extracted video ID.

2. API Call

const response = await fetch('/api/transcribe', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ videoUrl })
});

const data = await response.json();

Sends POST request to transcribe endpoint with video URL.

3. Response Validation

if (!response.ok) {
  throw new Error(data.error || 'Failed to fetch transcript');
}

if (!data?.transcript?.fullTranscript) {
  throw new Error('No transcript available for this video');
}

Checks for API errors and validates transcript data structure.

4. Success Handling

setTranscript(data.transcript.fullTranscript);
setHasTranscript(true);

toast({
  title: 'Success',
  description: 'Video loaded! You can now start chatting.',
  variant: 'default'
});

return true;

Updates store with transcript, shows success toast, returns true.

5. Error Handling

catch (error: any) {
  console.error('Error fetching transcript:', error);
  toast({
    title: 'Error',
    description: error.message || 'Failed to fetch transcript',
    variant: 'destructive'
  });
  return false;
}

Logs error, shows error toast, returns false.

6. Cleanup

finally {
  setIsLoadingTranscript(false);
}

Always resets loading state, regardless of success or failure.

Usage Example

import { useVideoTranscript } from '../hooks/use-video-transcript';

function VideoInput() {
  const { fetchTranscript } = useVideoTranscript();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await fetchTranscript();
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Input field */}
    </form>
  );
}

useGenerateVideoThreads Hook

Location: src/features/chat/hooks/use-generate-video-threads.ts

Purpose

Generates Twitter/X threads from video transcript using a separate useChat instance and parses the AI response into structured thread posts.

Dependencies

  • useChat: Vercel AI SDK hook for streaming
  • useToast: For displaying notifications
  • useVideoChatStore: For accessing transcript and updating threads
  • useEffect: For monitoring thread generation status

Implementation

export const useGenerateVideoThreads = () => {
  const { toast } = useToast();
  const {
    transcript,
    videoId,
    hasTranscript,
    setThreads,
    setGeneratingThreads,
    setThreadsModalOpen
  } = useVideoChatStore();

  // Separate useChat instance for thread generation
  const {
    messages: threadMessages,
    sendMessage,
    status: threadStatus
  } = useChat({
    id: 'thread-generation' // Unique ID
  });

  // useEffect for parsing
  // generateThreads function

  return { generateThreads, isGenerating: threadStatus === 'streaming' };
};

Separate Chat Instance

const {
  messages: threadMessages,
  sendMessage,
  status: threadStatus
} = useChat({
  id: 'thread-generation'
});

Uses a unique ID 'thread-generation' to separate from main chat messages. This prevents thread generation from interfering with the main chat conversation.

Thread Parsing Effect

Trigger Condition

useEffect(() => {
  if (threadStatus === 'ready' && threadMessages.length > 0) {
    // Parse threads
  }
}, [threadStatus, threadMessages, videoId, setThreads, ...]);

Runs when thread generation is complete (status === 'ready').

Extract Text from Message

const lastMessage = threadMessages[threadMessages.length - 1];
if (lastMessage.role === 'assistant') {
  let threadText = '';
  lastMessage.parts.forEach((part) => {
    if (part.type === 'text') {
      threadText += part.text;
    }
  });
}

Extracts text content from the last assistant message parts.

Parse Numbered Posts

const threadPosts: ThreadData[] = [];
const lines = threadText.split('\n').filter((line) => line.trim());

for (let i = 1; i <= 20; i++) {
  const threadLine = lines.find((line) => {
    const trimmed = line.trim();
    return trimmed.startsWith(`${i}:`) || trimmed.startsWith(`${i}.`);
  });

  if (threadLine) {
    let content = threadLine.replace(/^\d+[:.]\s*/, '').trim();
    if (content.length > 0) {
      threadPosts.push({
        post: i,
        content: content,
        total: 0
      });
    }
  } else {
    break; // No more posts found
  }
}

Dynamic parsing that finds numbered posts (1:, 2:, etc.) and extracts content. Stops when no more numbered posts are found.

Update Total Count

const totalPosts = threadPosts.length;
threadPosts.forEach((post) => {
  post.total = totalPosts;
});

Sets the total property for all posts after counting them.

Add Thumbnail

if (threadPosts.length > 0 && videoId) {
  threadPosts[0].thumbnail = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
}

Adds YouTube thumbnail to the first post using the video ID.

Success Handling

if (threadPosts.length > 0) {
  setThreads(threadPosts);
  toast({
    title: 'Success',
    description: `Generated ${threadPosts.length} thread posts!`
  });
} else {
  toast({
    title: 'Error',
    description: 'No thread posts were generated. Please try again.',
    variant: 'destructive'
  });
  setThreadsModalOpen(false);
}
setGeneratingThreads(false);

Updates store and shows appropriate toast based on success or failure.

Generate Threads Function

Validation

const generateThreads = () => {
  if (!hasTranscript || !transcript) {
    toast({
      title: 'Error',
      description: 'No transcript available. Please load a video first.',
      variant: 'destructive'
    });
    return;
  }
  // ...
};

Ensures transcript is loaded before generating threads.

Initialize State

setGeneratingThreads(true);
setThreads([]);
setThreadsModalOpen(true);

Sets loading state, clears previous threads, opens modal.

Thread Generation Prompt

const threadsPrompt = `Create a compelling X (Twitter) thread from this video content. Write a thread that tells the full story with engaging hooks and insights.

REQUIREMENTS:
- Start with a strong hook (e.g., "Here's the full story..." or "๐Ÿงต This will change how you think about...")
- Each post should be 200-250 characters (engaging but not too short)
- Generate 6-12 posts depending on content depth
- End with a strong conclusion and call-to-action
- Use emojis strategically
- Do not use hashtags in the thread

FORMAT: Write each post on a new line starting with "1:", "2:", etc.

Video content: ${transcript}

Write the complete thread now:`;

Detailed prompt that instructs AI to generate structured thread posts.

Send Message

sendMessage({
  text: threadsPrompt
});

Sends prompt to AI using the separate chat instance.

Usage Example

import { useGenerateVideoThreads } from '../hooks/use-generate-video-threads';

function ChatPage() {
  const { generateThreads, isGenerating } = useGenerateVideoThreads();

  return (
    <button onClick={generateThreads} disabled={isGenerating}>
      {isGenerating ? 'Generating...' : 'Generate Thread'}
    </button>
  );
}

Hook Interactions

Flow Diagram

1. useVideoTranscript
   โ†“
   fetchTranscript() โ†’ API call โ†’ setTranscript()
   โ†“
   hasTranscript = true
   โ†“
2. useGenerateVideoThreads
   โ†“
   generateThreads() โ†’ uses transcript โ†’ sends to AI
   โ†“
   useEffect monitors status
   โ†“
   Parses response โ†’ setThreads()

Store Updates

Both hooks update different parts of the store:

useVideoTranscript updates:

  • videoUrl
  • videoId
  • transcript
  • hasTranscript
  • isLoadingTranscript

useGenerateVideoThreads updates:

  • threads
  • generatingThreads
  • threadsModalOpen

Error Handling Patterns

Validation Errors

Both hooks validate input before processing:

  • Empty or invalid URLs
  • Missing transcripts
  • Invalid video IDs

API Errors

Proper error handling for network failures:

  • Try-catch blocks
  • Error messages in toast notifications
  • Boolean return values indicating success/failure

Loading States

Both hooks manage loading states:

  • isLoadingTranscript for transcript fetching
  • generatingThreads for thread generation
  • UI elements disabled during loading