From 17f6e8325ad871353b391c03371705eb6db414ab Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Sun, 9 Feb 2025 03:21:12 +1100 Subject: [PATCH] Add company data script, metadata file, and turbo.json env updates (#179) --- .../app/[username]/jobs-graph/page.js | 115 ++++++++++---- apps/registry/app/providers/ResumeProvider.js | 68 ++++++++- apps/registry/app/settings/page.tsx | 80 +++++++++- apps/registry/scripts/jobs/companyData.js | 144 ++++++++++++++++++ apps/registry/scripts/jobs/companyData.json | 34 +++++ turbo.json | 3 +- 6 files changed, 405 insertions(+), 39 deletions(-) create mode 100644 apps/registry/scripts/jobs/companyData.js create mode 100644 apps/registry/scripts/jobs/companyData.json diff --git a/apps/registry/app/[username]/jobs-graph/page.js b/apps/registry/app/[username]/jobs-graph/page.js index 5bcbad1..c3ee1eb 100644 --- a/apps/registry/app/[username]/jobs-graph/page.js +++ b/apps/registry/app/[username]/jobs-graph/page.js @@ -77,9 +77,14 @@ export default function Jobs({ params }) { const highlightText = useCallback((text, searchText) => { if (!searchText || !text) return text; const parts = text.toString().split(new RegExp(`(${searchText})`, 'gi')); - return parts.map((part, index) => - part.toLowerCase() === searchText.toLowerCase() ? - {part} : part + return parts.map((part, index) => + part.toLowerCase() === searchText.toLowerCase() ? ( + + {part} + + ) : ( + part + ) ); }, []); @@ -248,9 +253,18 @@ export default function Jobs({ params }) { data: { label: isResume ? (
- - + + Your Resume
@@ -260,29 +274,32 @@ export default function Jobs({ params }) { {jobData?.title || 'Unknown Position'}
- - + + {jobData?.company || 'Unknown Company'}
- +
{jobData?.type && ( -
- {jobData.type} -
+
{jobData.type}
)} {jobData?.remote && ( -
- {jobData.remote} -
+
{jobData.remote}
)} {jobData?.salary && ( -
- {jobData.salary} -
+
{jobData.salary}
)}
@@ -525,10 +542,20 @@ export default function Jobs({ params }) {

- {filterText ? highlightText(selectedNode.data.jobInfo.title, filterText) : selectedNode.data.jobInfo.title} + {filterText + ? highlightText( + selectedNode.data.jobInfo.title, + filterText + ) + : selectedNode.data.jobInfo.title}

- {filterText ? highlightText(selectedNode.data.jobInfo.company, filterText) : selectedNode.data.jobInfo.company} + {filterText + ? highlightText( + selectedNode.data.jobInfo.company, + filterText + ) + : selectedNode.data.jobInfo.company}

{selectedNode.data.jobInfo.type && ( @@ -612,7 +639,12 @@ export default function Jobs({ params }) { {selectedNode.data.jobInfo.description && (
- {filterText ? highlightText(selectedNode.data.jobInfo.description, filterText) : selectedNode.data.jobInfo.description} + {filterText + ? highlightText( + selectedNode.data.jobInfo.description, + filterText + ) + : selectedNode.data.jobInfo.description}
)} @@ -625,11 +657,19 @@ export default function Jobs({ params }) {
{selectedNode.data.jobInfo.skills.map( (skill, index) => ( -
- {filterText ? highlightText(skill.name, filterText) : skill.name} +
+ {filterText + ? highlightText(skill.name, filterText) + : skill.name} {skill.level && ( - • {filterText ? highlightText(skill.level, filterText) : skill.level} + •{' '} + {filterText + ? highlightText(skill.level, filterText) + : skill.level} )}
@@ -649,7 +689,9 @@ export default function Jobs({ params }) { {selectedNode.data.jobInfo.qualifications.map( (qual, index) => (
  • - {filterText ? highlightText(qual, filterText) : qual} + {filterText + ? highlightText(qual, filterText) + : qual}
  • ) )} @@ -666,9 +708,18 @@ export default function Jobs({ params }) { className="inline-flex items-center gap-1.5 rounded-md px-4 py-2 text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 shadow-sm transition-colors" > View Job Details - - + +
    @@ -701,7 +752,11 @@ export default function Jobs({ params }) { content: ''; position: absolute; inset: 0; - background: radial-gradient(circle at center, rgba(37, 99, 235, 0.1) 0%, transparent 70%); + background: radial-gradient( + circle at center, + rgba(37, 99, 235, 0.1) 0%, + transparent 70% + ); animation: pulse 3s ease-in-out infinite; } diff --git a/apps/registry/app/providers/ResumeProvider.js b/apps/registry/app/providers/ResumeProvider.js index c4b7322..6969e0b 100644 --- a/apps/registry/app/providers/ResumeProvider.js +++ b/apps/registry/app/providers/ResumeProvider.js @@ -26,7 +26,7 @@ export function useResume() { } export function ResumeProvider({ children, targetUsername }) { - const [session, setSession] = useState(null); + const [, setSession] = useState(null); const [resume, setResume] = useState(null); const [gistId, setGistId] = useState(null); const [loading, setLoading] = useState(true); @@ -170,11 +170,38 @@ export function ResumeProvider({ children, targetUsername }) { const updateGist = async (resumeContent) => { try { - if (!session?.provider_token) { - throw new Error('No GitHub access token available'); + const { + data: { session: currentSession }, + } = await supabase.auth.getSession(); + + if (!currentSession?.provider_token) { + // Try to get a fresh token + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + scopes: 'gist', + redirectTo: window.location.origin, + queryParams: { + access_type: 'offline', + prompt: 'consent', + }, + }, + }); + + if (error) throw error; + + // Wait for redirect + return; + } catch (error) { + console.error('GitHub authentication error:', error); + throw new Error( + 'Failed to authenticate with GitHub. Please try again.' + ); + } } - const octokit = new Octokit({ auth: session.provider_token }); + const octokit = new Octokit({ auth: currentSession.provider_token }); if (gistId) { await octokit.rest.gists.update({ @@ -210,11 +237,38 @@ export function ResumeProvider({ children, targetUsername }) { const createGist = async (sampleResume) => { try { - if (!session?.provider_token) { - throw new Error('No GitHub access token available'); + const { + data: { session: currentSession }, + } = await supabase.auth.getSession(); + + if (!currentSession?.provider_token) { + // Try to get a fresh token + try { + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'github', + options: { + scopes: 'gist', + redirectTo: window.location.origin, + queryParams: { + access_type: 'offline', + prompt: 'consent', + }, + }, + }); + + if (error) throw error; + + // Wait for redirect + return; + } catch (error) { + console.error('GitHub authentication error:', error); + throw new Error( + 'Failed to authenticate with GitHub. Please try again.' + ); + } } - const octokit = new Octokit({ auth: session.provider_token }); + const octokit = new Octokit({ auth: currentSession.provider_token }); const { data } = await octokit.rest.gists.create({ files: { [RESUME_GIST_NAME]: { diff --git a/apps/registry/app/settings/page.tsx b/apps/registry/app/settings/page.tsx index cff3cb0..fb5c933 100644 --- a/apps/registry/app/settings/page.tsx +++ b/apps/registry/app/settings/page.tsx @@ -49,6 +49,74 @@ export default function SettingsPage() {

    {username}

    )} +
    +

    GitHub Connection Status

    + {session.user.identities?.some( + (identity: any) => identity.provider === 'github' + ) ? ( +
    +

    ✓ Connected to GitHub

    +

    + Access Token Available:{' '} + {session.user.identities?.find( + (identity: any) => identity.provider === 'github' + )?.access_token + ? 'Yes' + : 'No'} +

    +
    +

    Provider Token: {session.provider_token ? 'Yes' : 'No'}

    + {session.provider_token && ( +

    + Token: {session.provider_token.substring(0, 8)}... + {session.provider_token.substring( + session.provider_token.length - 8 + )} +

    + )} +

    + App Metadata Token:{' '} + {session.user.app_metadata?.provider_token ? 'Yes' : 'No'} +

    +

    + Identity Token:{' '} + {session.user.identities?.find( + (identity: any) => identity.provider === 'github' + )?.provider_token + ? 'Yes' + : 'No'} +

    +
    + {!session.user.identities?.find( + (identity: any) => identity.provider === 'github' + )?.access_token && ( + + )} +
    + ) : ( +

    Not connected to GitHub

    + )} +

    User ID

    {session.user.id}

    @@ -60,7 +128,17 @@ export default function SettingsPage() {

    Debug Information

    -

    User Metadata

    +

    GitHub Identity

    +
    +                {JSON.stringify(
    +                  session.user.identities?.find(
    +                    (identity: any) => identity.provider === 'github'
    +                  ),
    +                  null,
    +                  2
    +                )}
    +              
    +

    User Metadata

                     {JSON.stringify(session.user.user_metadata, null, 2)}
                   
    diff --git a/apps/registry/scripts/jobs/companyData.js b/apps/registry/scripts/jobs/companyData.js new file mode 100644 index 0000000..5a1e36e --- /dev/null +++ b/apps/registry/scripts/jobs/companyData.js @@ -0,0 +1,144 @@ +require('dotenv').config({ path: __dirname + '/./../../.env' }); +const { createClient } = require('@supabase/supabase-js'); + +const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; +const supabaseKey = process.env.SUPABASE_KEY; +const supabase = createClient(supabaseUrl, supabaseKey); + +async function getCompanyInfo(companyName) { + console.log(`🔍 Fetching data for company: ${companyName}`); + + const requestBody = { + model: 'sonar-pro', + messages: [ + { + role: 'user', + content: `Generate a paragraph and recent news for the company: ${companyName}. Use markdown. Only add a heading for Recent news`, + }, + ], + max_tokens: 2000, + temperature: 0.9, + top_p: 0.9, + search_domain_filter: null, + return_images: false, + return_related_questions: false, + search_recency_filter: 'year', + top_k: 0, + stream: false, + presence_penalty: 0, + frequency_penalty: 1, + response_format: null, + }; + + const stringifiedBody = JSON.stringify(requestBody, null, 2); + console.log('📨 Request body:', stringifiedBody); + + const options = { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.PERPLEXITY_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: stringifiedBody, + }; + + const response = await fetch( + 'https://api.perplexity.ai/chat/completions', + options + ); + + if (!response.ok) { + console.error(`❌ API Error (${response.status}):`, await response.text()); + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + console.log( + `📝 Received response for ${companyName}:`, + JSON.stringify(data, null, 2) + ); + return data; +} + +async function main() { + console.log('🚀 Starting company data collection process...'); + + // Get jobs from the last 60 days + const sixtyDaysAgo = new Date(); + sixtyDaysAgo.setDate(sixtyDaysAgo.getDate() - 60); + + console.log(`📅 Fetching jobs since: ${sixtyDaysAgo.toISOString()}`); + + const { data: jobs, error: jobsError } = await supabase + .from('jobs') + .select('gpt_content') + .gte('created_at', sixtyDaysAgo.toISOString()); + + if (jobsError) { + console.error('❌ Error fetching jobs:', jobsError); + return; + } + + console.log(`📊 Found ${jobs.length} jobs to process`); + + // Extract unique companies + const companies = new Set(); + jobs.forEach((job) => { + try { + const content = + typeof job.gpt_content === 'string' + ? JSON.parse(job.gpt_content) + : job.gpt_content; + + if (content?.company) { + companies.add(content.company); + } + } catch (e) { + console.warn('⚠️ Error parsing job content:', e); + } + }); + + console.log(`🏢 Found ${companies.size} unique companies to process`); + + // Process each company + for (const company of companies) { + // Check if company already exists + const { data: existing } = await supabase + .from('companies') + .select('name') + .eq('name', company) + .single(); + + if (existing) { + console.log(`⏭️ Skipping ${company} - already exists in database`); + continue; + } + + console.log(`🔄 Processing company: ${company}`); + + try { + const companyData = await getCompanyInfo(company); + console.log({ companyData }); + // Save to database + const { error: saveError } = await supabase.from('companies').insert({ + name: company, + data: JSON.stringify(companyData), + }); + + if (saveError) { + console.error(`❌ Error saving ${company}:`, saveError); + } else { + console.log(`✅ Successfully saved data for ${company}`); + } + } catch (error) { + console.error(`❌ Error processing ${company}:`, error); + } + } + + console.log('🏁 Company data collection process completed!'); +} + +main().catch((error) => { + console.error('🚨 Fatal error:', error); + process.exit(1); +}); diff --git a/apps/registry/scripts/jobs/companyData.json b/apps/registry/scripts/jobs/companyData.json new file mode 100644 index 0000000..b812376 --- /dev/null +++ b/apps/registry/scripts/jobs/companyData.json @@ -0,0 +1,34 @@ +{ + "id": "88b06634-b264-4b49-8df4-77855be496ff", + "model": "sonar-pro", + "created": 1739021027, + "usage": { + "prompt_tokens": 21, + "completion_tokens": 261, + "total_tokens": 282, + "citation_tokens": 4096, + "num_search_queries": 1 + }, + "citations": [ + "https://shizune.co/investors/edtech-vc-funds-australia", + "https://addisons.com/article/updated-aic-open-source-seed-financing-documents-whats-changed/", + "https://www.consultancy.com.au/news/theme/labour-market", + "https://www.industry.gov.au/sites/default/files/2024-08/senate-order-13-2023-24-financial-year.pdf", + "https://info.beamible.com/action-high-workloads-playbook" + ], + "object": "chat.completion", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Beamible is an innovative workforce optimization platform designed to help businesses get the most out of their employees while also improving employee satisfaction and reducing burnout. The platform deconstructs work into its individual components, allowing organizations to reconstruct tasks in a way that better meets both business needs and employee preferences. By enabling flexible work arrangements and driving efficiency, Beamible helps companies reduce turnover, increase productivity, and create a more engaged workforce.\n\n## Recent News\n\n- **Beamible launches new AI-powered workforce analytics tool**: The company has introduced a new feature that uses artificial intelligence to analyze work patterns and provide actionable insights for optimizing team performance and employee well-being.\n\n- **Victorian government partners with Beamible for digital reskilling program**: As part of a $64 million initiative, the Victorian government has selected Beamible to help mid-career professionals transition into digital roles, leveraging the platform's expertise in workforce optimization and skills mapping[3].\n\n- **Beamible secures Series A funding**: The company has successfully raised a significant amount in its latest funding round, attracting investors impressed by its innovative approach to workforce management and its potential to transform the future of work." + }, + "delta": { + "role": "assistant", + "content": "" + } + } + ] +} diff --git a/turbo.json b/turbo.json index 5d60ebf..ca610f4 100644 --- a/turbo.json +++ b/turbo.json @@ -18,7 +18,8 @@ "VERCEL_ENV", "NEXT_PUBLIC_SUPABASE_URL", "NEXT_PUBLIC_SUPABASE_ANON_KEY", - "NEXT_PUBLIC_APP_URL" + "NEXT_PUBLIC_APP_URL", + "PERPLEXITY_API_KEY" ], "tasks": { "db:generate": {