From 2369b8d7f9f64f2866e956a92df0c9bc091a27c7 Mon Sep 17 00:00:00 2001 From: Harshith Sunku Date: Sun, 9 Feb 2025 21:13:05 +0530 Subject: [PATCH 1/9] Initial commit For linkedin scrapping in OMI Personas --- personas-open-source/src/app/chat/page.tsx | 28 +- personas-open-source/src/app/page.tsx | 356 +++++++++--------- .../src/components/ChatbotList.tsx | 62 +++ .../src/components/Footer.tsx | 13 + .../src/components/Header.tsx | 19 + .../src/components/InputArea.tsx | 28 ++ personas-open-source/src/types/profiles.ts | 144 +++++++ 7 files changed, 475 insertions(+), 175 deletions(-) create mode 100644 personas-open-source/src/components/ChatbotList.tsx create mode 100644 personas-open-source/src/components/Footer.tsx create mode 100644 personas-open-source/src/components/Header.tsx create mode 100644 personas-open-source/src/components/InputArea.tsx create mode 100644 personas-open-source/src/types/profiles.ts diff --git a/personas-open-source/src/app/chat/page.tsx b/personas-open-source/src/app/chat/page.tsx index 0c107963fc..87fb938070 100644 --- a/personas-open-source/src/app/chat/page.tsx +++ b/personas-open-source/src/app/chat/page.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Send, Settings, Share, ArrowLeft, BadgeCheck, X } from 'lucide-react'; +import { FaLinkedin } from 'react-icons/fa'; import { useSearchParams, useRouter } from 'next/navigation'; import { Suspense } from 'react'; import Link from 'next/link'; @@ -35,6 +36,7 @@ function ChatContent() { name: string; avatar: string; username?: string; + category?: string; } | null>(null); const [messages, setMessages] = useState([]); @@ -61,7 +63,8 @@ function ChatContent() { setBotData({ name: data.name, avatar: data.avatar, - username: data.username + username: data.username, + category: data.category }); } } catch (error) { @@ -76,6 +79,7 @@ function ChatContent() { const botName = botData?.name || 'Omi'; const botImage = botData?.avatar || '/omi-avatar.svg'; const username = botData?.username || ''; + const botCategory = botData?.category || ''; // Function to save messages to Firebase const saveMessagesToFirebase = useCallback(async () => { @@ -559,12 +563,22 @@ function ChatContent() {
- -

- {botName} - -

- + {botCategory === 'linkedin' ? ( + +

+ {botName} + +

+ + ) : botCategory === 'twitter' ? ( + +

+ {botName} + +

+ + ) : null + } {botName[0]} diff --git a/personas-open-source/src/app/page.tsx b/personas-open-source/src/app/page.tsx index 3f0e58afd5..8fdfba0fe8 100644 --- a/personas-open-source/src/app/page.tsx +++ b/personas-open-source/src/app/page.tsx @@ -1,45 +1,18 @@ 'use client'; -import { useEffect, useState } from 'react'; -import Link from 'next/link'; +import { SetStateAction, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { db } from '@/lib/firebase'; import { collection, addDoc, query, where, getDocs, orderBy, startAfter, limit } from 'firebase/firestore'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Search, Plus, BadgeCheck } from 'lucide-react'; -import { FaDiscord } from 'react-icons/fa'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Card, CardContent } from '@/components/ui/card'; import { toast } from 'sonner'; -import { PreorderBanner } from '@/components/shared/PreorderBanner'; import { Mixpanel } from '@/lib/mixpanel'; import { useInView } from 'react-intersection-observer'; - - -type Chatbot = { - id: string; - username?: string; - profile?: string; - avatar: string; - desc: string; - name: string; - sub_count?: number; - category: string; - created_at?: string; - verified?: boolean; -}; - -type TwitterProfile = { - profile: string; - rest_id: string; - avatar: string; - desc: string; - name: string; - friends: number; - sub_count: number; - id: string; -}; +import { Header } from '@/components/Header'; +import { InputArea } from '@/components/InputArea'; +import { ChatbotList } from '@/components/ChatbotList'; +import { Footer } from '@/components/Footer'; +import { Chatbot, TwitterProfile, LinkedinProfile } from '@/types/profiles'; +import { connection } from 'next/server'; const formatTwitterAvatarUrl = (url: string): string => { if (!url) return '/omi-avatar.svg'; @@ -100,11 +73,44 @@ export default function HomePage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); - const [twitterHandle, setTwitterHandle] = useState(''); const [isCreating, setIsCreating] = useState(false); const [hasMore, setHasMore] = useState(true); const [lastDoc, setLastDoc] = useState(null); const { ref, inView } = useInView(); + const [handle, setHandle] = useState(''); + + const handleInputChange = (e: { target: { value: SetStateAction; }; }) => { + setHandle(e.target.value); + }; + + const handleCreatePersona = () => { + const handlers = [ + fetchTwitterProfile, + fetchLinkedinProfile, + // Add new handlers here + ]; + + let successCount = 0; + let totalHandlers = handlers.length; + + const checkForErrors = () => { + if (successCount === 0) { + toast.error('No profiles found for the given handle.'); + } + }; + + handlers.forEach(handler => { + handler(handle).then(success => { + if (success) { + successCount++; + } + totalHandlers--; + if (totalHandlers === 0) { + checkForErrors(); + } + }); + }); + }; const BOTS_PER_PAGE = 50; @@ -125,7 +131,6 @@ export default function HomePage() { const chatbotsCollection = collection(db, 'plugins_data'); let q = query( chatbotsCollection, - where('category', '==', 'twitter'), orderBy('sub_count', 'desc') ); @@ -140,19 +145,15 @@ export default function HomePage() { // Single Map for all bots, keyed by lowercase username const allBotsMap = new Map(); - // Process all docs querySnapshot.docs.forEach(doc => { const bot = { id: doc.id, ...doc.data() } as Chatbot; - // Normalize username const normalizedUsername = bot.username?.toLowerCase().trim(); + const category = bot.category; if (!normalizedUsername || !bot.name) return; - const existingBot = allBotsMap.get(normalizedUsername); - // Only update if we don't have this bot or if this version has more followers - if (!existingBot || (bot.sub_count || 0) > (existingBot.sub_count || 0)) { - allBotsMap.set(normalizedUsername, bot); - } + const key = `${normalizedUsername}-${category}`; + allBotsMap.set(key, bot); }); const uniqueBots = Array.from(allBotsMap.values()); @@ -166,23 +167,22 @@ export default function HomePage() { // First add existing bots to master map prev.forEach(bot => { const username = bot.username?.toLowerCase().trim(); + const category = bot.category; if (username) { - masterMap.set(username, bot); + const key = `${username}-${category}`; + masterMap.set(key, bot); } }); - // Then add new bots, only if they have higher sub_count uniqueBots.forEach(bot => { const username = bot.username?.toLowerCase().trim(); + const category = bot.category; if (username) { - const existing = masterMap.get(username); - if (!existing || (bot.sub_count || 0) > (existing.sub_count || 0)) { - masterMap.set(username, bot); - } + const key = `${username}-${category}`; + masterMap.set(key, bot); } }); - // Convert back to array and sort by sub_count return Array.from(masterMap.values()) .sort((a, b) => (b.sub_count || 0) - (a.sub_count || 0)); }); @@ -219,8 +219,8 @@ export default function HomePage() { (bot.username && bot.username.toLowerCase().includes(searchQuery.toLowerCase())) ); - const fetchTwitterProfile = async () => { - if (!twitterHandle) return; + const fetchTwitterProfile = async (twitterHandle: string) => { + if (!twitterHandle) return false; const cleanHandle = twitterHandle.replace('@', ''); @@ -236,7 +236,7 @@ export default function HomePage() { const existingDoc = querySnapshot.docs[0]; toast.success('Profile already exists, redirecting...'); router.push(`/chat?id=${existingDoc.id}`); - return; + return true; } const profileResponse = await fetch(`https://${process.env.NEXT_PUBLIC_RAPIDAPI_HOST}/screenname.php?screenname=${cleanHandle}`, { @@ -246,8 +246,16 @@ export default function HomePage() { }, }); + if (!profileResponse.ok) { + return false; + } + const profileData: TwitterProfile = await profileResponse.json(); + if (!profileData || !profileData.name) { + return false; + } + const recentTweets = await fetchTwitterTimeline(cleanHandle); const formattedAvatarUrl = formatTwitterAvatarUrl(profileData.avatar); @@ -290,14 +298,124 @@ Recent activity on Twitter:\n"${enhancedDesc}" which you can use for your person toast.success('Profile saved successfully!'); router.push(`/chat?id=${docRef.id}`); + return true; } catch (firebaseError) { console.error('Firebase error:', firebaseError); toast.error('Failed to save profile'); + return false; } } catch (error) { console.error('Error fetching Twitter profile:', error); - toast.error('Failed to fetch Twitter profile'); + return false; + } finally { + setIsCreating(false); + } + }; + + const fetchLinkedinProfile = async (linkedinHandle: string) => { + if (!linkedinHandle) return false; + + const cleanHandle = linkedinHandle.replace('@', ''); + + setIsCreating(true); + try { + const q = query( + collection(db, 'plugins_data'), + where('username', '==', cleanHandle.toLowerCase()) + ); + const querySnapshot = await getDocs(q); + + if (!querySnapshot.empty) { + const existingDoc = querySnapshot.docs[0]; + toast.success('Profile already exists, redirecting...'); + router.push(`/chat?id=${existingDoc.id}`); + return true; + } + + const encodedHandle = encodeURIComponent(cleanHandle); + + const profileResponse = await fetch(`https://${process.env.NEXT_PUBLIC_LINKEDIN_API_HOST}/profile-data-connection-count-posts?username=${encodedHandle}`, { + headers: { + 'x-rapidapi-key': process.env.NEXT_PUBLIC_LINKEDIN_API_KEY!, + 'x-rapidapi-host': process.env.NEXT_PUBLIC_LINKEDIN_API_HOST!, + }, + }); + + if (!profileResponse.ok) { + return false; + } + + const profileData: LinkedinProfile = await profileResponse.json(); + + if (!profileData || !profileData.data.firstName) { + return false; + } + + const formattedAvatarUrl = profileData.data.profilePicture; + const fullName = `${profileData.data.firstName} ${profileData.data.lastName}`; + const headline = profileData.data.headline; + const summary = profileData.data.summary; + const positions = profileData.data.position.map(pos => `${pos.title} at ${pos.companyName} (${pos.start.year} - ${pos.end.year})`).join(', '); + const skills = Array.isArray(profileData.data.skills) ? profileData.data.skills.map(skill => skill.name).join(', ') : 'No skills available'; + const recentPosts = profileData.posts.map(post => post.text).join('\n'); + + const enhancedDesc = `${summary}\n\nPositions: ${positions}\n\nSkills: ${skills}\n\nRecent Posts:\n${recentPosts}`; + console.log('Enhanced description:', enhancedDesc); + + const extraPromptRules = process.env.NEXT_PUBLIC_EXTRA_PROMPT_RULES ?? ""; + + const fullChatPrompt = `You are ${fullName}, an AI persona. Here is some information about you: + +Name: ${fullName} +Headline: ${headline} +Summary: ${summary} +Positions: ${positions} +Skills: ${skills} + +Style: +- Interactive highly opinionated messages and questions. +- Reference your experiences and skills naturally. + +Rules: +- Never mention being AI +- No ** or formal language +- 2-3 sentences for response. Never more than 20 words. +- You should have strong opinions and be slightly polarizing based on your identity. +- You must make the conversation flow. You excel at coherent conversations. +${extraPromptRules} + +Recent activity on Linkedin:\n"${enhancedDesc}" which you can use for your personality. Responses should help the conversation go deeper. You have strong opinions, slightly controversial.`; + + + try { + const createdAtFormatted = formatDate(new Date().toISOString()); + const docRef = await addDoc(collection(db, 'plugins_data'), { + username: cleanHandle.toLowerCase().replace('@', ''), + avatar: formattedAvatarUrl, + profile: summary, + desc: enhancedDesc, + name: fullName, + sub_count: profileData.follower || 0, + category: 'linkedin', + created_at: createdAtFormatted, + chat_prompt: fullChatPrompt, + connection_count: profileData.connection, + }); + + toast.success('Profile saved successfully!'); + + router.push(`/chat?id=${docRef.id}`); + return true; + } catch (firebaseError) { + console.error('Firebase error:', firebaseError); + toast.error('Failed to save profile'); + return false; + } + + } catch (error) { + console.error('Error fetching LinkedIn profile:', error); + return false; } finally { setIsCreating(false); } @@ -305,124 +423,26 @@ Recent activity on Twitter:\n"${enhancedDesc}" which you can use for your person return (
- - {/* Header */} -
-
- - Logo - - - Take AI personas with you - - -
-
- - {/* Main Content */} +
- {/* Create Section */} -
-

AI personas

-

- Create new AI Twitter personalities -

-
- - {/* Input Area */} -
- setTwitterHandle(e.target.value)} - className="rounded-full bg-gray-800 text-white border-gray-700 focus:border-gray-600" - /> - -
- - {/* Chatbot List */} + {!loading && filteredChatbots.length > 0 && ( -
-
- - setSearchQuery(e.target.value)} - className="w-full bg-zinc-900 text-white rounded-full py-2 pl-10 pr-4 focus:outline-none focus:ring-2 focus:ring-zinc-700 border-0" - /> -
- -
- {filteredChatbots.map(bot => ( - handleChatbotClick(bot)} - className="hover:bg-zinc-800 transition-colors cursor-pointer bg-zinc-900 border-zinc-700" - > - - - - {bot.name[0]} - -
-
-

- {bot.name} - -

- - @{bot.username || bot.profile} - - {bot.sub_count !== undefined && ( - - {bot.sub_count.toLocaleString()} followers - - )} -
-

- {bot.profile || 'No profile available'} -

-
-
-
- ))} - - {hasMore && ( -
-
-
- )} -
-
+ )}
- - {/* Footer */} -
-
- Omi by Based Hardware © 2024 -
- - -
-
-
+
); } diff --git a/personas-open-source/src/components/ChatbotList.tsx b/personas-open-source/src/components/ChatbotList.tsx new file mode 100644 index 0000000000..73bc5037b1 --- /dev/null +++ b/personas-open-source/src/components/ChatbotList.tsx @@ -0,0 +1,62 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { BadgeCheck, Search } from 'lucide-react'; +import { FaLinkedin } from 'react-icons/fa'; + +export const ChatbotList = ({ chatbots, searchQuery, setSearchQuery, handleChatbotClick, hasMore, ref }) => ( +
+
+ + setSearchQuery(e.target.value)} + className="w-full bg-zinc-900 text-white rounded-full py-2 pl-10 pr-4 focus:outline-none focus:ring-2 focus:ring-zinc-700 border-0" + /> +
+ +
+ {chatbots.map(bot => ( + handleChatbotClick(bot)} + className="hover:bg-zinc-800 transition-colors cursor-pointer bg-zinc-900 border-zinc-700" + > + + + + {bot.name[0]} + +
+
+

+ {bot.name} + {bot.category === 'linkedin' ? ( + + ) : bot.category === 'twitter' ? ( + + ) : null} +

+ @{bot.username || bot.profile} + {bot.category === 'linkedin' && bot.connection_count !== undefined && ( + {bot.connection_count.toLocaleString()} connections + )} + {bot.sub_count !== undefined && ( + {bot.sub_count.toLocaleString()} followers + )} +
+

{bot.profile || 'No profile available'}

+
+
+
+ ))} + + {hasMore && ( +
+
+
+ )} +
+
+); diff --git a/personas-open-source/src/components/Footer.tsx b/personas-open-source/src/components/Footer.tsx new file mode 100644 index 0000000000..1d9764c70c --- /dev/null +++ b/personas-open-source/src/components/Footer.tsx @@ -0,0 +1,13 @@ +import { Button } from '@/components/ui/button'; + +export const Footer = () => ( +
+
+ Omi by Based Hardware © 2024 +
+ + +
+
+
+); diff --git a/personas-open-source/src/components/Header.tsx b/personas-open-source/src/components/Header.tsx new file mode 100644 index 0000000000..3a9d5e9082 --- /dev/null +++ b/personas-open-source/src/components/Header.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link'; + +export const Header = () => ( +
+
+ + Logo + + + Take AI personas with you + + +
+
+); diff --git a/personas-open-source/src/components/InputArea.tsx b/personas-open-source/src/components/InputArea.tsx new file mode 100644 index 0000000000..f737c060c2 --- /dev/null +++ b/personas-open-source/src/components/InputArea.tsx @@ -0,0 +1,28 @@ +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; + +interface InputAreaProps { + handle: string; + handleInputChange: (event: React.ChangeEvent) => void; + handleCreatePersona: () => void; + isCreating: boolean; +} + +export const InputArea: React.FC = ({ handle, handleInputChange, handleCreatePersona, isCreating }) => ( +
+ + +
+); diff --git a/personas-open-source/src/types/profiles.ts b/personas-open-source/src/types/profiles.ts new file mode 100644 index 0000000000..8b6984d019 --- /dev/null +++ b/personas-open-source/src/types/profiles.ts @@ -0,0 +1,144 @@ + +export type Chatbot = { + id: string; + username?: string; + profile?: string; + avatar: string; + desc: string; + name: string; + sub_count?: number; + category: string; + created_at?: string; + verified?: boolean; + connection_count?: number; +}; + +export type TwitterProfile = { + profile: string; + rest_id: string; + avatar: string; + desc: string; + name: string; + friends: number; + sub_count: number; + id: string; +}; + +export type LinkedinProfile = { + connection: number; + data: { + id: number; + urn: string; + username: string; + firstName: string; + lastName: string; + isTopVoice: boolean; + isCreator: boolean; + profilePicture: string; + backgroundImage: { width: number; height: number; url: string }[]; + summary: string; + headline: string; + geo: { country: string; city: string; full: string; countryCode: string }; + educations: { + start: { year: number; month: number; day: number }; + end: { year: number; month: number; day: number }; + fieldOfStudy: string; + degree: string; + grade: string; + schoolName: string; + description: string; + activities: string; + url: string; + schoolId: string; + logo: { url: string; width: number; height: number }[]; + }[]; + position: { + companyId: number; + companyName: string; + companyUsername: string; + companyURL: string; + companyLogo: string; + companyIndustry: string; + companyStaffCountRange: string; + title: string; + multiLocaleTitle: { en_US: string }; + multiLocaleCompanyName: { en_US: string }; + location: string; + description: string; + employmentType: string; + start: { year: number; month: number; day: number }; + end: { year: number; month: number; day: number }; + }[]; + fullPositions: { + companyId: number; + companyName: string; + companyUsername: string; + companyURL: string; + companyLogo: string; + companyIndustry: string; + companyStaffCountRange: string; + title: string; + multiLocaleTitle: { en_US: string }; + multiLocaleCompanyName: { en_US: string }; + location: string; + description: string; + employmentType: string; + start: { year: number; month: number; day: number }; + end: { year: number; month: number; day: number }; + }[]; + skills: { name: string; passedSkillAssessment: boolean; endorsementsCount: number }[]; + projects: Record; + supportedLocales: { country: string; language: string }[]; + multiLocaleFirstName: { en: string }; + multiLocaleLastName: { en: string }; + multiLocaleHeadline: { en: string }; + }; + follower: number; + posts: { + isBrandPartnership: boolean; + text: string; + totalReactionCount: number; + likeCount: number; + appreciationCount: number; + empathyCount: number; + InterestCount: number; + praiseCount: number; + commentsCount: number; + repostsCount: number; + postUrl: string; + postedAt: string; + postedDate: string; + postedDateTimestamp: number; + urn: string; + author: { + firstName: string; + lastName: string; + username: string; + url: string; + }; + company: Record; + document: Record; + celebration: Record; + poll: Record; + article: { + articleUrn: string; + title: string; + subtitle: string; + link: string; + newsletter: Record; + }; + entity: Record; + mentions: { + firstName: string; + lastName: string; + urn: string; + publicIdentifier: string; + }[]; + companyMentions: { + id: number; + name: string; + publicIdentifier: string; + url: string; + }[]; + }[]; +}; From 8902115a7098f1629141fbf09b9b1fff0a1cf801 Mon Sep 17 00:00:00 2001 From: Harshith Sunku Date: Sun, 9 Feb 2025 22:15:27 +0530 Subject: [PATCH 2/9] Updated Readme and added comments to a file --- personas-open-source/README.md | 7 +++ personas-open-source/src/app/page.tsx | 53 +++++++++++-------- .../src/components/ChatbotList.tsx | 19 +++++++ .../src/components/Footer.tsx | 13 +++++ .../src/components/Header.tsx | 13 +++++ .../src/components/InputArea.tsx | 18 +++++++ personas-open-source/src/types/profiles.ts | 22 ++++++++ 7 files changed, 123 insertions(+), 22 deletions(-) diff --git a/personas-open-source/README.md b/personas-open-source/README.md index d4bdec0066..82efd904a5 100644 --- a/personas-open-source/README.md +++ b/personas-open-source/README.md @@ -44,6 +44,11 @@ You'll need to obtain the following API keys and credentials: - Create an account at [Mixpanel](https://mixpanel.com) - Get your project token +5. **RapidAPI LinkedIn API** + - Subscribe to [LinkedIn API](https://rapidapi.com/rockapis-rockapis-default/api/linkedin-api8) + - Get your API key + - Access to profile data and posts + ## Setup Instructions 1. Clone the repository: @@ -72,6 +77,8 @@ You'll need to obtain the following API keys and credentials: NEXT_PUBLIC_RAPIDAPI_KEY=your_rapidapi_key OPENROUTER_API_KEY=your_openrouter_api_key NEXT_PUBLIC_MIXPANEL_TOKEN=your_mixpanel_token + NEXT_PUBLIC_LINKEDIN_API_HOST=your_rapidapi_linkedin_host + NEXT_PUBLIC_LINKEDIN_API_KEY=your_rapidapi_linkedin_key ``` 4. Run the development server: diff --git a/personas-open-source/src/app/page.tsx b/personas-open-source/src/app/page.tsx index 8fdfba0fe8..f22ad6a898 100644 --- a/personas-open-source/src/app/page.tsx +++ b/personas-open-source/src/app/page.tsx @@ -1,3 +1,10 @@ +/** + * @fileoverview Home Page Component for OMI Personas + * @description Main page component that handles persona creation, listing, and search functionality + * @author OMI Team + * @license MIT + */ + 'use client'; import { SetStateAction, useEffect, useState } from 'react'; @@ -12,7 +19,6 @@ import { InputArea } from '@/components/InputArea'; import { ChatbotList } from '@/components/ChatbotList'; import { Footer } from '@/components/Footer'; import { Chatbot, TwitterProfile, LinkedinProfile } from '@/types/profiles'; -import { connection } from 'next/server'; const formatTwitterAvatarUrl = (url: string): string => { if (!url) return '/omi-avatar.svg'; @@ -85,8 +91,8 @@ export default function HomePage() { const handleCreatePersona = () => { const handlers = [ - fetchTwitterProfile, fetchLinkedinProfile, + fetchTwitterProfile, // Add new handlers here ]; @@ -307,17 +313,24 @@ Recent activity on Twitter:\n"${enhancedDesc}" which you can use for your person } catch (error) { console.error('Error fetching Twitter profile:', error); + toast.error('Failed to fetch Twitter profile'); return false; } finally { setIsCreating(false); } }; + /** + * LinkedIn Profile Integration + * @description Fetches and processes LinkedIn profiles for persona creation + * @param {string} linkedinHandle - LinkedIn username to fetch + * @returns {Promise} Success status of profile creation + */ const fetchLinkedinProfile = async (linkedinHandle: string) => { if (!linkedinHandle) return false; - + const cleanHandle = linkedinHandle.replace('@', ''); - + setIsCreating(true); try { const q = query( @@ -325,7 +338,7 @@ Recent activity on Twitter:\n"${enhancedDesc}" which you can use for your person where('username', '==', cleanHandle.toLowerCase()) ); const querySnapshot = await getDocs(q); - + if (!querySnapshot.empty) { const existingDoc = querySnapshot.docs[0]; toast.success('Profile already exists, redirecting...'); @@ -333,35 +346,31 @@ Recent activity on Twitter:\n"${enhancedDesc}" which you can use for your person return true; } - const encodedHandle = encodeURIComponent(cleanHandle); - - const profileResponse = await fetch(`https://${process.env.NEXT_PUBLIC_LINKEDIN_API_HOST}/profile-data-connection-count-posts?username=${encodedHandle}`, { + const profileResponse = await fetch(`https://${process.env.NEXT_PUBLIC_LINKEDIN_API_HOST}/?username=${cleanHandle}`, { headers: { 'x-rapidapi-key': process.env.NEXT_PUBLIC_LINKEDIN_API_KEY!, 'x-rapidapi-host': process.env.NEXT_PUBLIC_LINKEDIN_API_HOST!, }, }); - + console.log('LinkedIn Profile Response:', profileResponse); if (!profileResponse.ok) { return false; } - + console.log('LinkedIn Profile Response 2:', profileResponse); const profileData: LinkedinProfile = await profileResponse.json(); - if (!profileData || !profileData.data.firstName) { + if (!profileData || !profileData.firstName) { return false; } - const formattedAvatarUrl = profileData.data.profilePicture; - const fullName = `${profileData.data.firstName} ${profileData.data.lastName}`; - const headline = profileData.data.headline; - const summary = profileData.data.summary; - const positions = profileData.data.position.map(pos => `${pos.title} at ${pos.companyName} (${pos.start.year} - ${pos.end.year})`).join(', '); - const skills = Array.isArray(profileData.data.skills) ? profileData.data.skills.map(skill => skill.name).join(', ') : 'No skills available'; - const recentPosts = profileData.posts.map(post => post.text).join('\n'); + const formattedAvatarUrl = profileData.profilePicture; + const fullName = `${profileData.firstName} ${profileData.lastName}`; + const headline = profileData.headline; + const summary = profileData.summary; + const positions = profileData.position.map(pos => `${pos.title} at ${pos.companyName} (${pos.start.year} - ${pos.end.year})`).join(', '); + const skills = Array.isArray(profileData.skills) ? profileData.skills.map(skill => skill.name).join(', ') : 'No skills available'; - const enhancedDesc = `${summary}\n\nPositions: ${positions}\n\nSkills: ${skills}\n\nRecent Posts:\n${recentPosts}`; - console.log('Enhanced description:', enhancedDesc); + const enhancedDesc = `${summary}\n\nPositions: ${positions}\n\nSkills: ${skills}`; const extraPromptRules = process.env.NEXT_PUBLIC_EXTRA_PROMPT_RULES ?? ""; @@ -396,11 +405,10 @@ Recent activity on Linkedin:\n"${enhancedDesc}" which you can use for your perso profile: summary, desc: enhancedDesc, name: fullName, - sub_count: profileData.follower || 0, + sub_count: profileData.sub_count || 0, category: 'linkedin', created_at: createdAtFormatted, chat_prompt: fullChatPrompt, - connection_count: profileData.connection, }); toast.success('Profile saved successfully!'); @@ -415,6 +423,7 @@ Recent activity on Linkedin:\n"${enhancedDesc}" which you can use for your perso } catch (error) { console.error('Error fetching LinkedIn profile:', error); + toast.error('Failed to fetch LinkedIn profile'); return false; } finally { setIsCreating(false); diff --git a/personas-open-source/src/components/ChatbotList.tsx b/personas-open-source/src/components/ChatbotList.tsx index 73bc5037b1..0dc0bf288a 100644 --- a/personas-open-source/src/components/ChatbotList.tsx +++ b/personas-open-source/src/components/ChatbotList.tsx @@ -1,8 +1,27 @@ +/** + * @fileoverview ChatbotList Component for OMI Personas + * @description Renders a searchable list of chatbot personas created from social media profiles + * @author HarshithSunku + * @license MIT + */ import { Card, CardContent } from '@/components/ui/card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { BadgeCheck, Search } from 'lucide-react'; import { FaLinkedin } from 'react-icons/fa'; +/** + * ChatbotList Component + * + * @component + * @param {Object} props - Component props + * @param {Array} props.chatbots - Array of chatbot objects to display + * @param {string} props.searchQuery - Current search query string + * @param {Function} props.setSearchQuery - Function to update search query + * @param {Function} props.handleChatbotClick - Click handler for chatbot selection + * @param {boolean} props.hasMore - Flag indicating if more chatbots can be loaded + * @param {React.RefObject} props.ref - Ref for infinite scroll functionality + * @returns {JSX.Element} Rendered ChatbotList component + */ export const ChatbotList = ({ chatbots, searchQuery, setSearchQuery, handleChatbotClick, hasMore, ref }) => (
diff --git a/personas-open-source/src/components/Footer.tsx b/personas-open-source/src/components/Footer.tsx index 1d9764c70c..de802bb324 100644 --- a/personas-open-source/src/components/Footer.tsx +++ b/personas-open-source/src/components/Footer.tsx @@ -1,5 +1,18 @@ +/** + * @fileoverview Footer Component for OMI Personas + * @description Renders the application footer with copyright and legal links + * @author HarshithSunku + * @license MIT + */ import { Button } from '@/components/ui/button'; +/** + * Footer Component + * + * @component + * @description Renders a footer with copyright notice and legal links + * @returns {JSX.Element} Rendered Footer component + */ export const Footer = () => (