Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing YouTube Transcriber Persona for Enhanced Video Content Interaction #500

Merged
merged 10 commits into from
Apr 11, 2024
59 changes: 55 additions & 4 deletions src/apps/chat/components/persona-selector/PersonaSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import TelegramIcon from '@mui/icons-material/Telegram';
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
import { useChatLLM } from '~/modules/llms/store-llms';

import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { navigateToPersonas } from '~/common/app.routes';
import { useChipBoolean } from '~/common/components/useChipBoolean';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { YouTubeURLInput } from './YouTubeURLInput';

import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
import { usePurposeStore } from './store-purposes';
import { v4 as uuidv4 } from 'uuid';


// 'special' purpose IDs, for tile hiding purposes
Expand Down Expand Up @@ -116,6 +118,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
const [searchQuery, setSearchQuery] = React.useState('');
const [filteredIDs, setFilteredIDs] = React.useState<SystemPurposeId[] | null>(null);
const [editMode, setEditMode] = React.useState(false);
const [isYouTubeTranscriberActive, setIsYouTubeTranscriberActive] = React.useState(false);


// external state
const showFinder = useUIPreferencesStore(state => state.showPersonaFinder);
Expand Down Expand Up @@ -153,10 +157,52 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa

// Handlers

const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
if (purposeId && setSystemPurposeId)
// Modify the handlePurposeChanged function to check for the YouTube Transcriber
const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
if (purposeId) {
if (purposeId === 'YouTubeTranscriber') {
// If the YouTube Transcriber tile is clicked, set the state accordingly
setIsYouTubeTranscriberActive(true);
} else {
setIsYouTubeTranscriberActive(false);
}
if (setSystemPurposeId) {
setSystemPurposeId(props.conversationId, purposeId);
}, [props.conversationId, setSystemPurposeId]);
}
}
}, [props.conversationId, setSystemPurposeId]);

React.useEffect(() => {
const isTranscriberActive = systemPurposeId === 'YouTubeTranscriber';
setIsYouTubeTranscriberActive(isTranscriberActive);
}, [systemPurposeId]);


// Implement handleAddMessage function
const handleAddMessage = (messageText: string) => {
// Retrieve the appendMessage action from the useChatStore
const { appendMessage } = useChatStore.getState();

const conversationId = props.conversationId;

// Create a new message object
const newMessage: DMessage = {
id: uuidv4(),
text: messageText,
sender: 'Bot',
avatar: null,
typing: false,
role: 'assistant' as 'assistant',
tokenCount: 0,
created: Date.now(),
updated: null,
};

// Append the new message to the conversation
appendMessage(conversationId, newMessage);
};



const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent<HTMLTextAreaElement>): void => {
// TODO: persist this change? Right now it's reset every time.
Expand Down Expand Up @@ -420,6 +466,11 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa

</Box>

{/* Check if the YouTube Transcriber persona is active */}
{isYouTubeTranscriberActive ? (
<YouTubeURLInput onSubmit={(url) => handleAddMessage(url) } isFetching={false} />
) : null}

</Box>
);
}
67 changes: 67 additions & 0 deletions src/apps/chat/components/persona-selector/YouTubeURLInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import { Button, Input } from '@mui/joy';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';

interface YouTubeURLInputProps {
onSubmit: (transcript: string) => void;
isFetching: boolean;
}

export const YouTubeURLInput: React.FC<YouTubeURLInputProps> = ({ onSubmit, isFetching }) => {
const [url, setUrl] = React.useState('');
const [submitFlag, setSubmitFlag] = React.useState(false);

// Function to extract video ID from URL
function extractVideoID(videoURL: string): string | null {
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
const match = videoURL.match(regExp);
return (match && match[1]?.length == 11) ? match[1] : null;
}

const videoID = extractVideoID(url);

// Callback function to handle new transcript
const handleNewTranscript = (newTranscript: YTVideoTranscript) => {
onSubmit(newTranscript.transcript); // Pass the transcript text to the onSubmit handler
setSubmitFlag(false); // Reset submit flag after handling
};

const { transcript, isFetching: isTranscriptFetching, isError, error } = useYouTubeTranscript(videoID && submitFlag ? videoID : null, handleNewTranscript);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUrl(event.target.value);
};

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Prevent form from causing a page reload
setSubmitFlag(true); // Set flag to indicate a submit action
};

return (
<form onSubmit={handleSubmit} style={{ marginBottom: '16px' }}>
<Input
required
type='url'
fullWidth
disabled={isFetching || isTranscriptFetching}
variant='outlined'
placeholder='Enter YouTube Video URL'
value={url}
onChange={handleChange}
startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />}
sx={{ mb: 1.5, backgroundColor: 'background.popup' }}
/>
<Button
type='submit'
variant='solid'
disabled={isFetching || isTranscriptFetching || !url}
loading={isFetching || isTranscriptFetching}
sx={{ minWidth: 140 }}
>
Get Transcript
</Button>
{isError && <div>Error fetching transcript. Please try again.</div>}
</form>
);
};
12 changes: 11 additions & 1 deletion src/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

export type SystemPurposeId = 'Catalyst' | 'Custom' | 'Designer' | 'Developer' | 'DeveloperPreview' | 'Executive' | 'Generic' | 'Scientist';
export type SystemPurposeId = 'Catalyst' | 'Custom' | 'Designer' | 'Developer' | 'DeveloperPreview' | 'Executive' | 'Generic' | 'Scientist' | 'YouTubeTranscriber';

export const defaultSystemPurposeId: SystemPurposeId = 'Generic';

Expand Down Expand Up @@ -110,4 +110,14 @@ Current date: {{LocaleNow}}
call: { starters: ['What\'s the task?', 'What can I do?', 'Ready for your task.', 'Yes?'] },
voices: { elevenLabs: { voiceId: 'flq6f7yk4E4fJM5XTYuZ' } },
},
YouTubeTranscriber: {
title: 'YouTube Transcriber',
description: 'Enter a YouTube URL to get the transcript and chat about the content.',
systemMessage: 'You are an expert in understanding video transcripts and answering questions about video content.',
symbol: '📺',
examples: ['Analyze the sentiment of this video', 'Summarize the key points of the lecture'],
call: { starters: ['Enter a YouTube URL to begin.', 'Ready to transcribe YouTube content.', 'Paste the YouTube link here.'] },
voices: { elevenLabs: { voiceId: 'z9fAnlkpzviPz146aGWa' } },
},

};