Skip to content

Conversation

@thomasdavis
Copy link
Member

@thomasdavis thomasdavis commented Feb 17, 2025

Summary by CodeRabbit

  • New Features

    • Launched an enhanced resume editing experience with multiple modes, including an intuitive AI chat interface and a dynamic GUI editor.
    • Introduced voice recording and text-to-speech capabilities, making it easier to interact with the resume editor.
    • Added a service to transcribe audio recordings into text for seamless content updates.
    • Implemented a new API endpoint for editing JSON resumes using AI suggestions.
  • Chores

    • Implemented broad improvements across the platform to boost performance, security, and maintainability.
    • Updated the OpenAI dependency to a newer version for enhanced functionality.

@changeset-bot
Copy link

changeset-bot bot commented Feb 17, 2025

⚠️ No Changeset found

Latest commit: 3da5697

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link

vercel bot commented Feb 17, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
jsonresume-org-homepage2 ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 17, 2025 1:03pm
jsonresume-org-registry ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 17, 2025 1:03pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2025

Warning

Rate limit exceeded

@thomasdavis has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 14 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 790031c and 3da5697.

📒 Files selected for processing (1)
  • apps/registry/app/api/transcribe/route.js (1 hunks)

Walkthrough

The pull request introduces a comprehensive set of best practices across various technologies and expands functionality within the application. It adds new API endpoints for AI-assisted resume editing and audio transcription, introduces new React components for chat and resume editing (including GUI and mode switching), and implements custom hooks for managing settings and speech synthesis. Additionally, the OpenAI package dependency is upgraded, and a job-processing script is enhanced to convert job descriptions into a structured JSON schema.

Changes

File(s) Change Summary
.windsurfrules Introduces best practices for Next.js, React, Tailwind CSS, Prisma, Supabase, Axios, Lodash, Express, OpenAI API, and UUID.
apps/.../api/chat/route.js and apps/.../api/transcribe/route.js Adds API endpoints: one for AI-assisted resume editing (invoking chat completions and tool calls) and one for audio transcription using the Whisper model, with comprehensive error handling and cleanup.
apps/.../components/{AIChatEditor, GuiEditor, ResumeEditor}.js Introduces new React components: an AI chat interface, a GUI-based resume editor with collapsible sections, and an enhanced resume editor supporting JSON, GUI, and AI chat modes, with improved state management and error handling.
apps/.../hooks/{useSettings, useSpeech}.js Adds custom React hooks to manage application settings via local storage and provide speech synthesis functionality with start/stop controls.
apps/registry/package.json Upgrades the "openai" dependency version from ^4.0.0 to ^4.28.0.
apps/.../scripts/jobs/gpted.js Adds a new function to convert job descriptions into a structured JSON schema with enhanced data fetching and error handling.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant API as Chat API Endpoint
    participant OA as OpenAI API
    C->>API: POST /api/chat with messages & resume
    API->>API: Verify API key & parse request
    API->>OA: Send chat completion request with system prompt
    OA-->>API: Return answer (with possible tool call details)
    API->>C: Respond with edited resume or plain message
Loading
sequenceDiagram
    participant C as Client
    participant API as Transcribe API Endpoint
    participant FS as File System
    participant OA as Whisper API
    C->>API: POST /api/transcribe with audio file
    API->>API: Validate and extract audio file
    API->>FS: Write audio buffer to temporary file
    API->>OA: Request transcription via Whisper model
    OA-->>API: Return transcription result
    API->>FS: Delete temporary file (async & sync)
    API->>C: Respond with transcription result
Loading

Possibly related PRs

Poem

I hopped through lines of code so bright,
Crafting APIs with chat and voice in flight.
My whiskers twitched over hooks and state,
Components and schemas all coming out great.
With bytes and bops, I celebrate today—
A bunny’s joyful dance in code’s display!
🐰✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@alwaysmeticulous
Copy link

alwaysmeticulous bot commented Feb 17, 2025

✅ Meticulous spotted zero visual differences across 62 screens tested: view results.

Meticulous simulated ~4 hours of user flows against your PR.

Expected differences? Click here. Last updated for commit 3da5697. This comment will update as new commits are pushed.

@coderabbitai coderabbitai bot changed the title @coderabbitai Add AI endpoints, UI components, hooks, and job conversion update Feb 17, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (21)
apps/registry/app/api/chat/route.js (4)

8-22: Validate or sanitize the systemPrompt content.
While the system prompt is a simple user instruction, there's a security consideration around dynamic prompt generation. If user-provided data is ever concatenated, consider sanitizing it to prevent prompt injection.


24-82: Consider dynamic JSON schema validation.
You define a strong resumeSchema here, but it’s unused in runtime checks. To enforce data integrity, apply a JSON schema validator or a similar check against the changes returned from the tool calls. This helps catch invalid or malicious updates.


220-234: Handle multiple tool calls more explicitly.
You currently stop after processing the first 'update_resume' function call. If multiple calls or tool names ever appear, they’ll be ignored. Consider either processing them all or ending early with a clear reason in the response.


242-245: Strengthen error handling for debugging.
When an error occurs, only the message is returned in JSON. Capturing stack traces or using a structured logging approach can help with debugging in production (while taking care not to leak sensitive data).

apps/registry/app/components/ResumeEditor.js (2)

190-198: Add user feedback on JSON parse errors.
When JSON parsing fails in handleJsonChange, you log the error but do not notify the user in the UI. Providing feedback, such as an error toast, would improve usability and debugging for end users.


300-306: Handle validation conflicts with AI suggestions.
The AIChatEditor might suggest changes conflicting with the existing schema. Consider hooking in a validation check before applying the onApplyChanges to prevent partial or invalid merges.

apps/registry/scripts/jobs/gpted.js (2)

218-270: Establish consistent naming for schema functions.
jobDescriptionToSchemaFunction is clear, but be consistent with other naming conventions (e.g., update_resume in your chat route). It helps maintain code readability and uniform patterns.


445-474: Consolidate multiple AI calls or track partial responses.
You have three sequential calls to OpenAI for the same job. If one fails, the overall job might remain incomplete. Consider a single orchestrated flow that can gracefully resume or store partial states. This reduces repeated context consumption and potential tokens usage.

apps/registry/app/components/GuiEditor.js (3)

7-26: Consider adding prop-type validations or TypeScript.

While FormSection is clearly structured, adding runtime checks via prop-types or using TypeScript would help ensure correct usage of props and enhance maintainability.


28-59: Propagate accessibility attributes in form fields.

Providing aria-label or more descriptive name attributes could improve screen reader support, especially for textarea inputs, ensuring an accessible form design.


61-86: Streamline array operations for better maintainability.

ArrayField is well-structured, but consider factoring out the array manipulation logic (e.g., slicing, splicing) into reusable utility functions to reduce repetition and improve readability across multiple sections.

apps/registry/app/hooks/useSettings.js (2)

9-19: Robust error handling for settings loading.

The try/catch block is good. For better fault tolerance, you could define a default fallback or inform users when corrupted data is detected, rather than silently catching the error.


21-29: Potential separation of concerns for local storage updates.

Consider decoupling local storage logic from the hook's core so the hook just manages state. This can make testing more straightforward, as you can swap in different storage solutions later.

apps/registry/app/components/ErrorBoundary.js (3)

5-9: Optional: Integrate with a monitoring system.

Capturing hasError and error in state is good for local handling. For production, consider integrating a logging or monitoring system (e.g., Sentry) to track these errors remotely.


15-17: Include user-friendly error reporting details.

While componentDidCatch logs the error to the console, you may want to provide an option for users to submit the error details (or aggregated logs) to further help debugging.


19-36: Enable customization of the fallback UI.

Currently, the fallback message is hardcoded. Exposing an optional fallback component or message prop can allow more flexible error presentation and a customized user experience.

apps/registry/app/api/transcribe/route.js (1)

34-46: Improve file cleanup reliability.

The current implementation attempts file cleanup twice, which is redundant. Consider using a more reliable cleanup approach.

-      // Clean up the temporary file
-      fs.unlink(tmpFilePath, (err) => {
-        if (err) console.error('Error deleting temporary file:', err);
-      });
-
       return Response.json({ text: transcription });
     } finally {
       // Ensure we try to clean up the temp file even if transcription fails
       try {
         fs.unlinkSync(tmpFilePath);
       } catch (e) {
-        // Ignore errors during cleanup
+        console.error('Error during file cleanup:', e);
       }
     }
apps/registry/app/hooks/useSpeech.js (2)

38-46: Consider using a more efficient retry strategy.

The current implementation uses fixed retry intervals. Consider using an exponential backoff strategy for better efficiency.

-      const retryTimes = [100, 500, 1000, 2000];
-      retryTimes.forEach(time => {
-        setTimeout(loadVoices, time);
-      });
+      let retryCount = 0;
+      const maxRetries = 4;
+      const retryWithBackoff = () => {
+        if (retryCount < maxRetries) {
+          const delay = Math.pow(2, retryCount) * 100;
+          setTimeout(() => {
+            loadVoices();
+            retryCount++;
+            retryWithBackoff();
+          }, delay);
+        }
+      };
+      retryWithBackoff();

100-108: Document the Chrome workaround.

The Chrome-specific workaround should be documented to explain why it's necessary.

-        // Chrome workaround: speak a short text first
+        // Chrome bug workaround: Speaking an empty utterance first prevents the main utterance
+        // from being cut off at the beginning. See: https://bugs.chromium.org/p/chromium/issues/detail?id=335907
         const warmup = new SpeechSynthesisUtterance('');
apps/registry/app/components/AIChatEditor.js (2)

8-15: Consider using a dedicated storage hook.

The localStorage logic could be extracted into a custom hook for better reusability.

+const useLocalStorage = (key, initialValue, maxItems) => {
+  const [storedValue, setStoredValue] = useState(() => {
+    try {
+      const item = localStorage.getItem(key);
+      if (item) {
+        const parsed = JSON.parse(item);
+        return maxItems ? parsed.slice(-maxItems) : parsed;
+      }
+      return initialValue;
+    } catch (error) {
+      console.error(error);
+      return initialValue;
+    }
+  });
+
+  useEffect(() => {
+    try {
+      localStorage.setItem(key, JSON.stringify(storedValue));
+    } catch (error) {
+      console.error(error);
+    }
+  }, [key, storedValue]);
+
+  return [storedValue, setStoredValue];
+};

-  const [messages, setMessages] = useState(() => {
-    const savedMessages = localStorage.getItem('resumeAiChatMessages');
-    if (savedMessages) {
-      const parsed = JSON.parse(savedMessages);
-      return parsed.slice(-30); // Keep only last 30 messages
-    }
-    return [];
-  });
+  const [messages, setMessages] = useLocalStorage('resumeAiChatMessages', [], 30);

272-278: Add ARIA labels for better accessibility.

The input field should have proper ARIA labels for screen readers.

 <input
   type="text"
   value={inputMessage}
   onChange={(e) => setInputMessage(e.target.value)}
   onKeyPress={(e) => e.key === 'Enter' && handleSubmitMessage(inputMessage)}
   placeholder="Type your message..."
+  aria-label="Message input"
+  role="textbox"
   className="flex-1 p-2 border rounded"
 />
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 20d6452 and 3e4d4f1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • .windsurfrules (1 hunks)
  • apps/registry/app/api/chat/route.js (1 hunks)
  • apps/registry/app/api/transcribe/route.js (1 hunks)
  • apps/registry/app/components/AIChatEditor.js (1 hunks)
  • apps/registry/app/components/ErrorBoundary.js (1 hunks)
  • apps/registry/app/components/GuiEditor.js (1 hunks)
  • apps/registry/app/components/ResumeEditor.js (2 hunks)
  • apps/registry/app/hooks/useSettings.js (1 hunks)
  • apps/registry/app/hooks/useSpeech.js (1 hunks)
  • apps/registry/package.json (1 hunks)
  • apps/registry/scripts/jobs/gpted.js (3 hunks)
🔇 Additional comments (8)
apps/registry/app/api/chat/route.js (2)

1-6: Consider adding checks for missing environment variables.
Currently, if process.env.OPENAI_API_KEY is undefined, the OpenAI instance setup might fail without a clear error message. Adding a fallback or explicit error throw could improve stability and troubleshooting.

Would you like me to generate a script or additional logic to confirm that OPENAI_API_KEY is set at deployment?


84-108: Add a request body shape mention or TypeScript interface.
The code expects { messages, currentResume } from req.json(). Profile your code for potential missing fields. Using TypeScript or runtime checks ensures robust request handling.

apps/registry/app/components/ResumeEditor.js (1)

14-43: Validate or sanitize default values.
The defaultResume provides blank fields, which is good as a fallback. However, ensure that further down the pipeline, empty strings do not propagate incorrectly. For instance, certain required fields might inadvertently remain empty in final exports.

apps/registry/scripts/jobs/gpted.js (2)

280-280: Evaluate performance impacts of a 20-day window.
Changing from 24 hours to 20 days (line 280) might result in retrieving significantly more data. Consider caching, pagination, or chunked processing to avoid timeouts or crashes on large datasets.

Would you like me to generate a script to measure how many rows are being fetched from your Supabase table over a 20-day period?


379-385: Watch out for custom model naming.
You’re using 'gpt-4o-mini', which might be an internal or custom name. Ensure it matches a real or authorized model in your environment. If this is a placeholder, add fallback logic or documentation.

apps/registry/app/hooks/useSettings.js (1)

3-7: Handle SSR scenarios gracefully.

When using Next.js, localStorage isn't available during SSR. Consider using defensive checks (e.g., typeof window !== 'undefined') or a fallback to maintain consistent behavior on the server and client.

.windsurfrules (1)

1-90: LGTM! Comprehensive best practices guide.

The best practices guide covers essential aspects of each technology and provides valuable guidelines for maintaining code quality and performance.

apps/registry/package.json (1)

99-99: Dependency Upgrade: OpenAI Package Version Updated

The openai dependency is now upgraded to "^4.28.0", which aligns with the new API endpoints and functionalities introduced in this PR. Please verify that this version is fully compatible with all modules that interact with the OpenAI API and review the official release notes for any potential breaking changes or new features that might affect the application.

Comment on lines 118 to 182
const handleApplyChanges = useCallback(
(changes) => {
const newResume = { ...resume };

// Helper function to intelligently merge arrays
const mergeArrays = (existingArray = [], newArray = [], key = 'name') => {
const result = [...existingArray];

newArray.forEach((newItem) => {
// For deletion (marked with _delete flag)
if (newItem._delete) {
const index = result.findIndex(
(item) =>
item[key] === newItem[key] ||
(item.startDate === newItem.startDate &&
item.endDate === newItem.endDate),
);
if (index !== -1) {
result.splice(index, 1);
}
return;
}

// For updates or additions
const existingIndex = result.findIndex(
(item) =>
item[key] === newItem[key] ||
(item.startDate === newItem.startDate &&
item.endDate === newItem.endDate),
);

if (existingIndex !== -1) {
// Update existing item
result[existingIndex] = { ...result[existingIndex], ...newItem };
} else {
// Add new item
result.push(newItem);
}
});

return result;
};

// Process each section of changes
Object.entries(changes).forEach(([section, value]) => {
if (Array.isArray(value)) {
// Handle array sections (work, education, etc.)
newResume[section] = mergeArrays(
resume[section],
value,
section === 'skills' ? 'name' : 'name', // Use appropriate key for matching
);
} else if (typeof value === 'object' && value !== null) {
// Handle nested objects
newResume[section] = { ...resume[section], ...value };
} else {
// Handle primitive values
newResume[section] = value;
}
});

setResume(newResume);
},
[resume],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refine the array merge logic.
The mergeArrays method is useful, but it’s keyed to certain fields like name, startDate, and endDate. If the user wants to remove or rename those fields, or if the resume schema changes significantly, edge cases could arise. Consider making the matching criteria more explicit or configurable so that future schema changes don’t break the function.

Comment on lines +49 to +51
if (iframeRef.current) {
iframeRef.current.srcdoc = htmlString;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider sanitizing HTML for security.
Using iframeRef.current.srcdoc = htmlString can introduce an XSS risk if htmlString is ever user-provided or untrusted. Implement a sanitization step (e.g., DOMPurify) if there's any chance that the content might come from untrusted sources.

- iframeRef.current.srcdoc = htmlString;
+ iframeRef.current.srcdoc = sanitizeHtmlString(htmlString);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +400 to +411
const { data: companyData, error: companyError } = await supabase
.from('companies')
.select()
.eq('name', company);

const parsedCompanyData = JSON.parse(companyData[0].data);

// exit if no company data
if (companyError || !parsedCompanyData) {
// exit node
process.exit(1);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid abrupt process.exit(1) in production.
Terminating the Node.js process can disrupt server uptime. Instead, handle errors gracefully, log them, or move this logic to a script exit only if this code is guaranteed to run as a one-off job.

Comment on lines +88 to +1120
export default function GuiEditor({ resume, onChange }) {
const updateBasics = (field, value) => {
onChange({
...resume,
basics: {
...resume.basics,
[field]: value,
},
});
};

const updateLocation = (field, value) => {
onChange({
...resume,
basics: {
...resume.basics,
location: {
...resume.basics?.location,
[field]: value,
},
},
});
};

const updateWorkExperience = (index, field, value) => {
const newWork = [...(resume.work || [])];
newWork[index] = { ...newWork[index], [field]: value };
onChange({ ...resume, work: newWork });
};

const addWorkExperience = () => {
onChange({
...resume,
work: [
...(resume.work || []),
{
name: '',
position: '',
startDate: '',
endDate: '',
location: '',
url: '',
summary: '',
description: '',
highlights: [],
},
],
});
};

const removeWorkExperience = (index) => {
const newWork = [...(resume.work || [])];
newWork.splice(index, 1);
onChange({ ...resume, work: newWork });
};

const addHighlight = (workIndex) => {
const newWork = [...(resume.work || [])];
newWork[workIndex] = {
...newWork[workIndex],
highlights: [...(newWork[workIndex].highlights || []), ''],
};
onChange({ ...resume, work: newWork });
};

const updateHighlight = (workIndex, highlightIndex, value) => {
const newWork = [...(resume.work || [])];
newWork[workIndex].highlights[highlightIndex] = value;
onChange({ ...resume, work: newWork });
};

const removeHighlight = (workIndex, highlightIndex) => {
const newWork = [...(resume.work || [])];
newWork[workIndex].highlights.splice(highlightIndex, 1);
onChange({ ...resume, work: newWork });
};

const updateProfiles = (index, field, value) => {
const newProfiles = [...(resume.basics?.profiles || [])];
newProfiles[index] = { ...newProfiles[index], [field]: value };
onChange({
...resume,
basics: {
...resume.basics,
profiles: newProfiles,
},
});
};

const addProfile = () => {
onChange({
...resume,
basics: {
...resume.basics,
profiles: [
...(resume.basics?.profiles || []),
{ network: '', username: '', url: '' },
],
},
});
};

const removeProfile = (index) => {
const newProfiles = [...(resume.basics?.profiles || [])];
newProfiles.splice(index, 1);
onChange({
...resume,
basics: {
...resume.basics,
profiles: newProfiles,
},
});
};

const updateVolunteer = (index, field, value) => {
const newVolunteer = [...(resume.volunteer || [])];
newVolunteer[index] = { ...newVolunteer[index], [field]: value };
onChange({ ...resume, volunteer: newVolunteer });
};

const updateEducation = (index, field, value) => {
const newEducation = [...(resume.education || [])];
newEducation[index] = { ...newEducation[index], [field]: value };
onChange({ ...resume, education: newEducation });
};

const updateAwards = (index, field, value) => {
const newAwards = [...(resume.awards || [])];
newAwards[index] = { ...newAwards[index], [field]: value };
onChange({ ...resume, awards: newAwards });
};

const updateCertificates = (index, field, value) => {
const newCertificates = [...(resume.certificates || [])];
newCertificates[index] = { ...newCertificates[index], [field]: value };
onChange({ ...resume, certificates: newCertificates });
};

const updatePublications = (index, field, value) => {
const newPublications = [...(resume.publications || [])];
newPublications[index] = { ...newPublications[index], [field]: value };
onChange({ ...resume, publications: newPublications });
};

const updateSkills = (index, field, value) => {
const newSkills = [...(resume.skills || [])];
newSkills[index] = { ...newSkills[index], [field]: value };
onChange({ ...resume, skills: newSkills });
};

const updateLanguages = (index, field, value) => {
const newLanguages = [...(resume.languages || [])];
newLanguages[index] = { ...newLanguages[index], [field]: value };
onChange({ ...resume, languages: newLanguages });
};

const updateInterests = (index, field, value) => {
const newInterests = [...(resume.interests || [])];
newInterests[index] = { ...newInterests[index], [field]: value };
onChange({ ...resume, interests: newInterests });
};

const updateReferences = (index, field, value) => {
const newReferences = [...(resume.references || [])];
newReferences[index] = { ...newReferences[index], [field]: value };
onChange({ ...resume, references: newReferences });
};

const updateProjects = (index, field, value) => {
const newProjects = [...(resume.projects || [])];
newProjects[index] = { ...newProjects[index], [field]: value };
onChange({ ...resume, projects: newProjects });
};

const addArrayItem = (section, template) => {
onChange({
...resume,
[section]: [...(resume[section] || []), template],
});
};

const removeArrayItem = (section, index) => {
const newArray = [...(resume[section] || [])];
newArray.splice(index, 1);
onChange({ ...resume, [section]: newArray });
};

return (
<div className="h-full overflow-auto p-4">
<FormSection title="Basic Information" defaultOpen={true}>
<div className="grid grid-cols-2 gap-4">
<FormField
label="Full Name"
value={resume.basics?.name}
onChange={(value) => updateBasics('name', value)}
placeholder="John Doe"
/>
<FormField
label="Label"
value={resume.basics?.label}
onChange={(value) => updateBasics('label', value)}
placeholder="Software Engineer"
/>
<FormField
label="Email"
type="email"
value={resume.basics?.email}
onChange={(value) => updateBasics('email', value)}
placeholder="john@example.com"
/>
<FormField
label="Phone"
value={resume.basics?.phone}
onChange={(value) => updateBasics('phone', value)}
placeholder="+1 (123) 456-7890"
/>
<FormField
label="Website"
type="url"
value={resume.basics?.url}
onChange={(value) => updateBasics('url', value)}
placeholder="https://example.com"
/>
<FormField
label="Image URL"
type="url"
value={resume.basics?.image}
onChange={(value) => updateBasics('image', value)}
placeholder="https://example.com/photo.jpg"
/>
</div>
<FormField
label="Summary"
type="textarea"
value={resume.basics?.summary}
onChange={(value) => updateBasics('summary', value)}
placeholder="A brief summary about yourself..."
/>
</FormSection>

<FormSection title="Location">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Address"
type="textarea"
value={resume.basics?.location?.address}
onChange={(value) => updateLocation('address', value)}
placeholder="123 Main St"
/>
<FormField
label="Postal Code"
value={resume.basics?.location?.postalCode}
onChange={(value) => updateLocation('postalCode', value)}
placeholder="12345"
/>
<FormField
label="City"
value={resume.basics?.location?.city}
onChange={(value) => updateLocation('city', value)}
placeholder="San Francisco"
/>
<FormField
label="Region"
value={resume.basics?.location?.region}
onChange={(value) => updateLocation('region', value)}
placeholder="California"
/>
<FormField
label="Country Code"
value={resume.basics?.location?.countryCode}
onChange={(value) => updateLocation('countryCode', value)}
placeholder="US"
/>
</div>
</FormSection>

<FormSection title="Work Experience">
<ArrayField
items={resume.work || []}
onAdd={addWorkExperience}
onRemove={removeWorkExperience}
addLabel="Add Work Experience"
renderItem={(item, index) => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Company Name"
value={item.name}
onChange={(value) =>
updateWorkExperience(index, 'name', value)
}
placeholder="Company Name"
/>
<FormField
label="Position"
value={item.position}
onChange={(value) =>
updateWorkExperience(index, 'position', value)
}
placeholder="Job Title"
/>
<FormField
label="Start Date"
value={item.startDate}
onChange={(value) =>
updateWorkExperience(index, 'startDate', value)
}
placeholder="YYYY-MM"
/>
<FormField
label="End Date"
value={item.endDate}
onChange={(value) =>
updateWorkExperience(index, 'endDate', value)
}
placeholder="YYYY-MM or Present"
/>
<FormField
label="Location"
value={item.location}
onChange={(value) =>
updateWorkExperience(index, 'location', value)
}
placeholder="City, Country"
/>
<FormField
label="Website"
type="url"
value={item.url}
onChange={(value) =>
updateWorkExperience(index, 'url', value)
}
placeholder="https://company.com"
/>
</div>
<FormField
label="Summary"
type="textarea"
value={item.summary}
onChange={(value) =>
updateWorkExperience(index, 'summary', value)
}
placeholder="Brief summary of your role and responsibilities..."
/>
<FormField
label="Description"
type="textarea"
value={item.description}
onChange={(value) =>
updateWorkExperience(index, 'description', value)
}
placeholder="Detailed description of your work experience..."
/>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Highlights
</label>
{(item.highlights || []).map((highlight, hIndex) => (
<div key={hIndex} className="flex gap-2">
<input
type="text"
value={highlight}
onChange={(e) =>
updateHighlight(index, hIndex, e.target.value)
}
className="flex-1 px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Achievement or responsibility..."
/>
<Button
variant="outline"
size="sm"
onClick={() => removeHighlight(index, hIndex)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={() => addHighlight(index)}
className="w-full flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" />
Add Highlight
</Button>
</div>
</div>
)}
/>
</FormSection>

<FormSection title="Profiles">
<ArrayField
items={resume.basics?.profiles || []}
onAdd={addProfile}
onRemove={removeProfile}
addLabel="Add Profile"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Network"
value={item.network}
onChange={(value) => updateProfiles(index, 'network', value)}
placeholder="Twitter, LinkedIn, etc."
/>
<FormField
label="Username"
value={item.username}
onChange={(value) => updateProfiles(index, 'username', value)}
placeholder="johndoe"
/>
<FormField
label="URL"
type="url"
value={item.url}
onChange={(value) => updateProfiles(index, 'url', value)}
placeholder="https://twitter.com/johndoe"
/>
</div>
)}
/>
</FormSection>

<FormSection title="Volunteer Experience">
<ArrayField
items={resume.volunteer || []}
onAdd={() =>
addArrayItem('volunteer', {
organization: '',
position: '',
url: '',
startDate: '',
endDate: '',
summary: '',
highlights: [],
})
}
onRemove={(index) => removeArrayItem('volunteer', index)}
addLabel="Add Volunteer Experience"
renderItem={(item, index) => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Organization"
value={item.organization}
onChange={(value) =>
updateVolunteer(index, 'organization', value)
}
placeholder="Organization Name"
/>
<FormField
label="Position"
value={item.position}
onChange={(value) =>
updateVolunteer(index, 'position', value)
}
placeholder="Volunteer Position"
/>
<FormField
label="Start Date"
value={item.startDate}
onChange={(value) =>
updateVolunteer(index, 'startDate', value)
}
placeholder="YYYY-MM"
/>
<FormField
label="End Date"
value={item.endDate}
onChange={(value) => updateVolunteer(index, 'endDate', value)}
placeholder="YYYY-MM or Present"
/>
<FormField
label="URL"
type="url"
value={item.url}
onChange={(value) => updateVolunteer(index, 'url', value)}
placeholder="https://organization.com"
/>
</div>
<FormField
label="Summary"
type="textarea"
value={item.summary}
onChange={(value) => updateVolunteer(index, 'summary', value)}
placeholder="Description of your volunteer work..."
/>
<ArrayField
items={item.highlights || []}
onAdd={() => {
const newVolunteer = [...(resume.volunteer || [])];
newVolunteer[index] = {
...newVolunteer[index],
highlights: [...(newVolunteer[index].highlights || []), ''],
};
onChange({ ...resume, volunteer: newVolunteer });
}}
onRemove={(highlightIndex) => {
const newVolunteer = [...(resume.volunteer || [])];
newVolunteer[index].highlights.splice(highlightIndex, 1);
onChange({ ...resume, volunteer: newVolunteer });
}}
addLabel="Add Highlight"
renderItem={(highlight, highlightIndex) => (
<FormField
value={highlight}
onChange={(value) => {
const newVolunteer = [...(resume.volunteer || [])];
newVolunteer[index].highlights[highlightIndex] = value;
onChange({ ...resume, volunteer: newVolunteer });
}}
placeholder="Achievement or responsibility..."
/>
)}
/>
</div>
)}
/>
</FormSection>

<FormSection title="Education">
<ArrayField
items={resume.education || []}
onAdd={() =>
addArrayItem('education', {
institution: '',
area: '',
studyType: '',
startDate: '',
endDate: '',
score: '',
courses: [],
})
}
onRemove={(index) => removeArrayItem('education', index)}
addLabel="Add Education"
renderItem={(item, index) => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Institution"
value={item.institution}
onChange={(value) =>
updateEducation(index, 'institution', value)
}
placeholder="University Name"
/>
<FormField
label="Area"
value={item.area}
onChange={(value) => updateEducation(index, 'area', value)}
placeholder="Field of Study"
/>
<FormField
label="Study Type"
value={item.studyType}
onChange={(value) =>
updateEducation(index, 'studyType', value)
}
placeholder="Bachelor, Master, etc."
/>
<FormField
label="Start Date"
value={item.startDate}
onChange={(value) =>
updateEducation(index, 'startDate', value)
}
placeholder="YYYY-MM"
/>
<FormField
label="End Date"
value={item.endDate}
onChange={(value) => updateEducation(index, 'endDate', value)}
placeholder="YYYY-MM or Present"
/>
<FormField
label="Score"
value={item.score}
onChange={(value) => updateEducation(index, 'score', value)}
placeholder="Grade or GPA"
/>
</div>
<ArrayField
items={item.courses || []}
onAdd={() => {
const newEducation = [...(resume.education || [])];
newEducation[index] = {
...newEducation[index],
courses: [...(newEducation[index].courses || []), ''],
};
onChange({ ...resume, education: newEducation });
}}
onRemove={(courseIndex) => {
const newEducation = [...(resume.education || [])];
newEducation[index].courses.splice(courseIndex, 1);
onChange({ ...resume, education: newEducation });
}}
addLabel="Add Course"
renderItem={(course, courseIndex) => (
<FormField
value={course}
onChange={(value) => {
const newEducation = [...(resume.education || [])];
newEducation[index].courses[courseIndex] = value;
onChange({ ...resume, education: newEducation });
}}
placeholder="Course name or code"
/>
)}
/>
</div>
)}
/>
</FormSection>

<FormSection title="Awards">
<ArrayField
items={resume.awards || []}
onAdd={() =>
addArrayItem('awards', {
title: '',
date: '',
awarder: '',
summary: '',
})
}
onRemove={(index) => removeArrayItem('awards', index)}
addLabel="Add Award"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Title"
value={item.title}
onChange={(value) => updateAwards(index, 'title', value)}
placeholder="Award Title"
/>
<FormField
label="Date"
value={item.date}
onChange={(value) => updateAwards(index, 'date', value)}
placeholder="YYYY-MM-DD"
/>
<FormField
label="Awarder"
value={item.awarder}
onChange={(value) => updateAwards(index, 'awarder', value)}
placeholder="Organization"
/>
<FormField
label="Summary"
type="textarea"
value={item.summary}
onChange={(value) => updateAwards(index, 'summary', value)}
placeholder="Description of the award..."
/>
</div>
)}
/>
</FormSection>

<FormSection title="Certificates">
<ArrayField
items={resume.certificates || []}
onAdd={() =>
addArrayItem('certificates', {
name: '',
date: '',
issuer: '',
url: '',
})
}
onRemove={(index) => removeArrayItem('certificates', index)}
addLabel="Add Certificate"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updateCertificates(index, 'name', value)}
placeholder="Certificate Name"
/>
<FormField
label="Date"
value={item.date}
onChange={(value) => updateCertificates(index, 'date', value)}
placeholder="YYYY-MM-DD"
/>
<FormField
label="Issuer"
value={item.issuer}
onChange={(value) => updateCertificates(index, 'issuer', value)}
placeholder="Issuing Organization"
/>
<FormField
label="URL"
type="url"
value={item.url}
onChange={(value) => updateCertificates(index, 'url', value)}
placeholder="https://certificate.com"
/>
</div>
)}
/>
</FormSection>

<FormSection title="Publications">
<ArrayField
items={resume.publications || []}
onAdd={() =>
addArrayItem('publications', {
name: '',
publisher: '',
releaseDate: '',
url: '',
summary: '',
})
}
onRemove={(index) => removeArrayItem('publications', index)}
addLabel="Add Publication"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updatePublications(index, 'name', value)}
placeholder="Publication Title"
/>
<FormField
label="Publisher"
value={item.publisher}
onChange={(value) =>
updatePublications(index, 'publisher', value)
}
placeholder="Publisher Name"
/>
<FormField
label="Release Date"
value={item.releaseDate}
onChange={(value) =>
updatePublications(index, 'releaseDate', value)
}
placeholder="YYYY-MM-DD"
/>
<FormField
label="URL"
type="url"
value={item.url}
onChange={(value) => updatePublications(index, 'url', value)}
placeholder="https://publication.com"
/>
<FormField
label="Summary"
type="textarea"
value={item.summary}
onChange={(value) =>
updatePublications(index, 'summary', value)
}
placeholder="Description of the publication..."
/>
</div>
)}
/>
</FormSection>

<FormSection title="Skills">
<ArrayField
items={resume.skills || []}
onAdd={() =>
addArrayItem('skills', {
name: '',
level: '',
keywords: [],
})
}
onRemove={(index) => removeArrayItem('skills', index)}
addLabel="Add Skill"
renderItem={(item, index) => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updateSkills(index, 'name', value)}
placeholder="Skill Name"
/>
<FormField
label="Level"
value={item.level}
onChange={(value) => updateSkills(index, 'level', value)}
placeholder="Beginner, Intermediate, Expert"
/>
</div>
<ArrayField
items={item.keywords || []}
onAdd={() => {
const newSkills = [...(resume.skills || [])];
newSkills[index] = {
...newSkills[index],
keywords: [...(newSkills[index].keywords || []), ''],
};
onChange({ ...resume, skills: newSkills });
}}
onRemove={(keywordIndex) => {
const newSkills = [...(resume.skills || [])];
newSkills[index].keywords.splice(keywordIndex, 1);
onChange({ ...resume, skills: newSkills });
}}
addLabel="Add Keyword"
renderItem={(keyword, keywordIndex) => (
<FormField
value={keyword}
onChange={(value) => {
const newSkills = [...(resume.skills || [])];
newSkills[index].keywords[keywordIndex] = value;
onChange({ ...resume, skills: newSkills });
}}
placeholder="Related technology or tool"
/>
)}
/>
</div>
)}
/>
</FormSection>

<FormSection title="Languages">
<ArrayField
items={resume.languages || []}
onAdd={() =>
addArrayItem('languages', {
language: '',
fluency: '',
})
}
onRemove={(index) => removeArrayItem('languages', index)}
addLabel="Add Language"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Language"
value={item.language}
onChange={(value) => updateLanguages(index, 'language', value)}
placeholder="Language Name"
/>
<FormField
label="Fluency"
value={item.fluency}
onChange={(value) => updateLanguages(index, 'fluency', value)}
placeholder="Native Speaker, Fluent, Intermediate, etc."
/>
</div>
)}
/>
</FormSection>

<FormSection title="Interests">
<ArrayField
items={resume.interests || []}
onAdd={() =>
addArrayItem('interests', {
name: '',
keywords: [],
})
}
onRemove={(index) => removeArrayItem('interests', index)}
addLabel="Add Interest"
renderItem={(item, index) => (
<div className="space-y-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updateInterests(index, 'name', value)}
placeholder="Interest Name"
/>
<ArrayField
items={item.keywords || []}
onAdd={() => {
const newInterests = [...(resume.interests || [])];
newInterests[index] = {
...newInterests[index],
keywords: [...(newInterests[index].keywords || []), ''],
};
onChange({ ...resume, interests: newInterests });
}}
onRemove={(keywordIndex) => {
const newInterests = [...(resume.interests || [])];
newInterests[index].keywords.splice(keywordIndex, 1);
onChange({ ...resume, interests: newInterests });
}}
addLabel="Add Keyword"
renderItem={(keyword, keywordIndex) => (
<FormField
value={keyword}
onChange={(value) => {
const newInterests = [...(resume.interests || [])];
newInterests[index].keywords[keywordIndex] = value;
onChange({ ...resume, interests: newInterests });
}}
placeholder="Related activity or topic"
/>
)}
/>
</div>
)}
/>
</FormSection>

<FormSection title="References">
<ArrayField
items={resume.references || []}
onAdd={() =>
addArrayItem('references', {
name: '',
reference: '',
})
}
onRemove={(index) => removeArrayItem('references', index)}
addLabel="Add Reference"
renderItem={(item, index) => (
<div className="grid grid-cols-2 gap-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updateReferences(index, 'name', value)}
placeholder="Reference Name"
/>
<FormField
label="Reference"
type="textarea"
value={item.reference}
onChange={(value) =>
updateReferences(index, 'reference', value)
}
placeholder="Reference text..."
/>
</div>
)}
/>
</FormSection>

<FormSection title="Projects">
<ArrayField
items={resume.projects || []}
onAdd={() =>
addArrayItem('projects', {
name: '',
description: '',
highlights: [],
keywords: [],
startDate: '',
endDate: '',
url: '',
roles: [],
})
}
onRemove={(index) => removeArrayItem('projects', index)}
addLabel="Add Project"
renderItem={(item, index) => (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
label="Name"
value={item.name}
onChange={(value) => updateProjects(index, 'name', value)}
placeholder="Project Name"
/>
<FormField
label="URL"
type="url"
value={item.url}
onChange={(value) => updateProjects(index, 'url', value)}
placeholder="https://project.com"
/>
<FormField
label="Start Date"
value={item.startDate}
onChange={(value) =>
updateProjects(index, 'startDate', value)
}
placeholder="YYYY-MM"
/>
<FormField
label="End Date"
value={item.endDate}
onChange={(value) => updateProjects(index, 'endDate', value)}
placeholder="YYYY-MM or Present"
/>
</div>
<FormField
label="Description"
type="textarea"
value={item.description}
onChange={(value) =>
updateProjects(index, 'description', value)
}
placeholder="Project description..."
/>
<ArrayField
items={item.highlights || []}
onAdd={() => {
const newProjects = [...(resume.projects || [])];
newProjects[index] = {
...newProjects[index],
highlights: [...(newProjects[index].highlights || []), ''],
};
onChange({ ...resume, projects: newProjects });
}}
onRemove={(highlightIndex) => {
const newProjects = [...(resume.projects || [])];
newProjects[index].highlights.splice(highlightIndex, 1);
onChange({ ...resume, projects: newProjects });
}}
addLabel="Add Highlight"
renderItem={(highlight, highlightIndex) => (
<FormField
value={highlight}
onChange={(value) => {
const newProjects = [...(resume.projects || [])];
newProjects[index].highlights[highlightIndex] = value;
onChange({ ...resume, projects: newProjects });
}}
placeholder="Project achievement or feature..."
/>
)}
/>
</div>
)}
/>
</FormSection>
</div>
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Reduce repetitive update logic with a higher-order utility.

Throughout the main GuiEditor component, numerous sections follow a similar pattern of copying an array or object, modifying it, and passing it back via onChange. Consider implementing a single function like updateSection(sectionKey, idx, field, value) or a specialized reducer to reduce code duplication and potential errors.

Comment on lines 6 to 8
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify OpenAI API key configuration.

Ensure that the OPENAI_API_KEY environment variable is properly configured in your deployment environment.


🏁 Script executed:

#!/bin/bash
# Description: Check if OPENAI_API_KEY is configured in environment variables.
# Note: This script will only work if the environment variables are accessible.

if [ -z "$OPENAI_API_KEY" ]; then
  echo "Warning: OPENAI_API_KEY is not set"
else
  echo "OPENAI_API_KEY is configured"
fi

Length of output: 94


OPENAI_API_KEY is missing in the current environment

The verification script output shows that the OPENAI_API_KEY environment variable is not set. Please configure this variable in both your deployment and local testing environments to prevent runtime issues. Ensure that your environment configuration (e.g., a .env file or deployment settings) provides a valid API key before running the application.

  • Location: apps/registry/app/api/transcribe/route.js (lines 6-8)
  • Action: Set a valid OPENAI_API_KEY in your environment configuration

Comment on lines +19 to +23
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
await writeFile(tmpFilePath, buffer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding file size and type validation.

The current implementation doesn't validate the audio file's size or type before processing, which could lead to unnecessary processing of invalid files.

 const bytes = await audioFile.arrayBuffer();
 const buffer = Buffer.from(bytes);
+const maxSize = 25 * 1024 * 1024; // 25MB limit
+if (buffer.length > maxSize) {
+  return Response.json({ error: 'Audio file too large. Maximum size is 25MB.' }, { status: 400 });
+}
+if (!audioFile.type.startsWith('audio/')) {
+  return Response.json({ error: 'Invalid file type. Only audio files are allowed.' }, { status: 400 });
+}
 const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
 await writeFile(tmpFilePath, buffer);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
await writeFile(tmpFilePath, buffer);
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const maxSize = 25 * 1024 * 1024; // 25MB limit
if (buffer.length > maxSize) {
return Response.json({ error: 'Audio file too large. Maximum size is 25MB.' }, { status: 400 });
}
if (!audioFile.type.startsWith('audio/')) {
return Response.json({ error: 'Invalid file type. Only audio files are allowed.' }, { status: 400 });
}
const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
await writeFile(tmpFilePath, buffer);

Comment on lines 143 to 187
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks = [];

recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const formData = new FormData();
formData.append('audio', blob);

try {
setIsLoading(true);
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData,
});

if (!response.ok) throw new Error('Failed to transcribe audio');

const data = await response.json();
if (data.error) throw new Error(data.error);

// Instead of setting input, directly send the transcribed message
handleSubmitMessage(data.text);
} catch (error) {
console.error('Transcription error:', error);
addSystemMessage('Failed to transcribe audio: ' + error.message, 'error');
} finally {
setIsLoading(false);
}

// Clean up the stream
stream.getTracks().forEach(track => track.stop());
};

recorder.start();
setMediaRecorder(recorder);
setIsRecording(true);
} catch (error) {
console.error('Error accessing microphone:', error);
addSystemMessage('Failed to access microphone: ' + error.message, 'error');
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for unsupported browsers.

The audio recording functionality should check for browser support before attempting to access the microphone.

 const startRecording = async () => {
   try {
+    if (!navigator.mediaDevices?.getUserMedia) {
+      throw new Error('Audio recording is not supported in this browser');
+    }
     const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const formData = new FormData();
formData.append('audio', blob);
try {
setIsLoading(true);
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error('Failed to transcribe audio');
const data = await response.json();
if (data.error) throw new Error(data.error);
// Instead of setting input, directly send the transcribed message
handleSubmitMessage(data.text);
} catch (error) {
console.error('Transcription error:', error);
addSystemMessage('Failed to transcribe audio: ' + error.message, 'error');
} finally {
setIsLoading(false);
}
// Clean up the stream
stream.getTracks().forEach(track => track.stop());
};
recorder.start();
setMediaRecorder(recorder);
setIsRecording(true);
} catch (error) {
console.error('Error accessing microphone:', error);
addSystemMessage('Failed to access microphone: ' + error.message, 'error');
}
};
const startRecording = async () => {
try {
if (!navigator.mediaDevices?.getUserMedia) {
throw new Error('Audio recording is not supported in this browser');
}
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
const formData = new FormData();
formData.append('audio', blob);
try {
setIsLoading(true);
const response = await fetch('/api/transcribe', {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error('Failed to transcribe audio');
const data = await response.json();
if (data.error) throw new Error(data.error);
// Instead of setting input, directly send the transcribed message
handleSubmitMessage(data.text);
} catch (error) {
console.error('Transcription error:', error);
addSystemMessage('Failed to transcribe audio: ' + error.message, 'error');
} finally {
setIsLoading(false);
}
// Clean up the stream
stream.getTracks().forEach(track => track.stop());
};
recorder.start();
setMediaRecorder(recorder);
setIsRecording(true);
} catch (error) {
console.error('Error accessing microphone:', error);
addSystemMessage('Failed to access microphone: ' + error.message, 'error');
}
};

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
apps/registry/app/api/chat/route.js (3)

4-18: Enhance the system prompt with validation rules.

The system prompt could be improved by adding validation rules for:

  1. Required fields in each section
  2. Valid values for enums (e.g., employment type)
  3. URL format validation
  4. Phone number format validation

20-26: Improve error handling with specific messages.

The error response could be more informative by:

  1. Using a more specific status code (e.g., 503 for service unavailable is good)
  2. Including an error code in the response
  3. Adding a more descriptive message about the OpenAI API being unavailable
 return NextResponse.json(
-  { message: 'API not available during build' },
+  {
+    error: 'OPENAI_API_UNAVAILABLE',
+    message: 'OpenAI API is not available. Please check your configuration.',
+    details: 'Missing API key in environment variables'
+  },
   { status: 503 },
 );

35-164: Add rate limiting and timeout handling.

Consider implementing:

  1. Rate limiting to prevent API abuse
  2. Timeout handling for long-running requests
  3. Retry logic for failed API calls
 const completion = await openai.chat.completions.create({
+  maxRetries: 3,
+  timeout: 30000, // 30 seconds
   model: 'gpt-4',
   messages: [
apps/registry/app/components/ResumeEditor.js (1)

14-75: Add schema validation for resume data.

Consider adding Zod or Yup schema validation to:

  1. Validate the resume structure against the schema
  2. Provide type safety and runtime validation
  3. Handle malformed data gracefully
apps/registry/app/components/AIChatEditor.js (2)

7-25: Consider using a reducer for complex state management.

The component manages multiple related states. Consider:

  1. Using useReducer for better state management
  2. Implementing proper cleanup for localStorage
  3. Adding state persistence error handling
+const chatReducer = (state, action) => {
+  switch (action.type) {
+    case 'ADD_MESSAGE':
+      return { ...state, messages: [...state.messages, action.message] };
+    case 'SET_LOADING':
+      return { ...state, isLoading: action.loading };
+    default:
+      return state;
+  }
+};

339-409: Add visual feedback for settings changes.

Consider adding:

  1. Toast notifications for settings updates
  2. Visual confirmation when settings are saved
  3. Error handling for failed settings updates
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4d4f1 and f2730a9.

📒 Files selected for processing (6)
  • apps/registry/app/api/chat/route.js (1 hunks)
  • apps/registry/app/api/transcribe/route.js (1 hunks)
  • apps/registry/app/components/AIChatEditor.js (1 hunks)
  • apps/registry/app/components/ResumeEditor.js (2 hunks)
  • apps/registry/app/hooks/useSpeech.js (1 hunks)
  • apps/registry/scripts/jobs/gpted.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/registry/app/hooks/useSpeech.js
  • apps/registry/app/api/transcribe/route.js
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build
🔇 Additional comments (3)
apps/registry/app/components/ResumeEditor.js (1)

123-159: Refine the array merge logic.

The mergeArrays method is useful, but it's keyed to certain fields like name, startDate, and endDate. If the user wants to remove or rename those fields, or if the resume schema changes significantly, edge cases could arise. Consider making the matching criteria more explicit or configurable so that future schema changes don't break the function.

apps/registry/app/components/AIChatEditor.js (1)

160-217: Add error handling for unsupported browsers.

The audio recording functionality should check for browser support before attempting to access the microphone.

apps/registry/scripts/jobs/gpted.js (1)

400-411: Avoid abrupt process.exit(1) in production.

Terminating the Node.js process can disrupt server uptime. Instead, handle errors gracefully, log them, or move this logic to a script exit only if this code is guaranteed to run as a one-off job.

Comment on lines 166 to 203
const response = completion.choices[0].message;

// If no tool calls, just return the message
if (!response.tool_calls) {
return NextResponse.json({
message: response.content,
suggestedChanges: null,
});
}

// Process tool calls
for (const toolCall of response.tool_calls) {
if (toolCall.function.name === 'update_resume') {
try {
const { changes, explanation } = JSON.parse(
toolCall.function.arguments,
);
return NextResponse.json({
message: explanation,
suggestedChanges: changes,
});
} catch (error) {
console.error('Error parsing tool call arguments:', error);
throw error;
}
}
}

// Fallback response if no relevant tool calls
return NextResponse.json({
message: response.content,
suggestedChanges: null,
});
} catch (error) {
console.error('Error:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement more granular error handling.

The current error handling could be improved by:

  1. Distinguishing between different types of errors (parsing, API, validation)
  2. Providing more specific error messages in the response
  3. Adding error logging with proper error context
 } catch (error) {
-  console.error('Error:', error);
-  return NextResponse.json({ error: error.message }, { status: 500 });
+  console.error('Error processing chat completion:', {
+    error,
+    messages,
+    currentResume
+  });
+  const status = error.status || 500;
+  const message = error.message || 'Internal server error';
+  return NextResponse.json({
+    error: {
+      type: error.name || 'UnknownError',
+      message,
+      details: process.env.NODE_ENV === 'development' ? error.stack : undefined
+    }
+  }, { status });
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +210 to +250
<div className="inline-flex rounded-lg border bg-gray-50/50 p-1 shadow-sm">
<Button
variant={editorMode === 'json' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('json')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'json'}
>
<Code className="w-4 h-4" aria-hidden="true" />
<span>JSON</span>
</Button>
<Button
variant={editorMode === 'gui' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('gui')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'gui'}
>
<Layout className="w-4 h-4" aria-hidden="true" />
<span>Form</span>
</Button>
<Button
variant={editorMode === 'ai' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('ai')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors relative"
aria-pressed={editorMode === 'ai'}
>
<span
className="relative flex items-center"
aria-hidden="true"
>
<span className="absolute -top-1 -right-1 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
</span>
<span>AI Chat</span>
</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility for editor mode switching.

Consider adding:

  1. ARIA labels for better screen reader support
  2. Keyboard navigation between modes
  3. Focus management when switching modes
 <div className="inline-flex rounded-lg border bg-gray-50/50 p-1 shadow-sm">
+  <div role="tablist" aria-label="Editor modes">
     <Button
       variant={editorMode === 'json' ? 'default' : 'ghost'}
       size="sm"
       onClick={() => setEditorMode('json')}
+      role="tab"
+      aria-selected={editorMode === 'json'}
+      aria-controls="json-editor"
       className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="inline-flex rounded-lg border bg-gray-50/50 p-1 shadow-sm">
<Button
variant={editorMode === 'json' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('json')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'json'}
>
<Code className="w-4 h-4" aria-hidden="true" />
<span>JSON</span>
</Button>
<Button
variant={editorMode === 'gui' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('gui')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'gui'}
>
<Layout className="w-4 h-4" aria-hidden="true" />
<span>Form</span>
</Button>
<Button
variant={editorMode === 'ai' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('ai')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors relative"
aria-pressed={editorMode === 'ai'}
>
<span
className="relative flex items-center"
aria-hidden="true"
>
<span className="absolute -top-1 -right-1 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
</span>
<span>AI Chat</span>
</Button>
</div>
<div className="inline-flex rounded-lg border bg-gray-50/50 p-1 shadow-sm">
<div role="tablist" aria-label="Editor modes">
<Button
variant={editorMode === 'json' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('json')}
role="tab"
aria-selected={editorMode === 'json'}
aria-controls="json-editor"
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'json'}
>
<Code className="w-4 h-4" aria-hidden="true" />
<span>JSON</span>
</Button>
<Button
variant={editorMode === 'gui' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('gui')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors"
aria-pressed={editorMode === 'gui'}
>
<Layout className="w-4 h-4" aria-hidden="true" />
<span>Form</span>
</Button>
<Button
variant={editorMode === 'ai' ? 'default' : 'ghost'}
size="sm"
onClick={() => setEditorMode('ai')}
className="flex items-center gap-2 min-w-[90px] justify-center transition-colors relative"
aria-pressed={editorMode === 'ai'}
>
<span
className="relative flex items-center"
aria-hidden="true"
>
<span className="absolute -top-1 -right-1 flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
</span>
</span>
<span>AI Chat</span>
</Button>
</div>
</div>

const chat = await openai.chat.completions.create({
model: 'gpt-4o-2024-08-06',
temperature: 0.8,
model: 'gpt-4o-mini',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix invalid OpenAI model name.

The model name 'gpt-4o-mini' appears to be invalid. Use a valid model name like 'gpt-4' or 'gpt-3.5-turbo'.

-model: 'gpt-4o-mini',
+model: 'gpt-4',

Also applies to: 430-430, 451-451

Comment on lines +459 to +471
console.log({ chat3, content });
const { error } = await supabase
.from('jobs')
.update({
gpt_content: details,
gpt_content_json_extended: jobJson2,
gpt_content_full: content,
})
.eq('id', job.id);
console.log({ error });
} catch (e) {
console.log({ e });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve database operation error handling.

Consider implementing:

  1. Proper error logging with error context
  2. Retry mechanism for transient failures
  3. Transaction rollback on failure
-console.log({ error });
+const maxRetries = 3;
+let retryCount = 0;
+while (retryCount < maxRetries) {
+  try {
+    const { error } = await supabase
+      .from('jobs')
+      .update({
+        gpt_content: details,
+        gpt_content_json_extended: jobJson2,
+        gpt_content_full: content,
+      })
+      .eq('id', job.id);
+    if (!error) break;
+    console.error('Database update failed:', {
+      error,
+      jobId: job.id,
+      retryCount,
+    });
+    retryCount++;
+    await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
+  } catch (e) {
+    console.error('Unexpected error during database update:', {
+      error: e,
+      jobId: job.id,
+      retryCount,
+    });
+    retryCount++;
+    await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
+  }
+}

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
apps/registry/app/api/transcribe/route.js (2)

1-12: ⚠️ Potential issue

Add missing NextResponse import.

The code uses NextResponse but it's not imported.

Add this import at the beginning of the file:

+import { NextResponse } from 'next/server';

Verify OpenAI API key configuration.

Ensure that the OPENAI_API_KEY environment variable is properly configured in your deployment environment.


18-28: 🛠️ Refactor suggestion

Consider adding file size and type validation.

The current implementation doesn't validate the audio file's size or type before processing, which could lead to unnecessary processing of invalid files.

 const bytes = await audioFile.arrayBuffer();
 const buffer = Buffer.from(bytes);
+const maxSize = 25 * 1024 * 1024; // 25MB limit
+if (buffer.length > maxSize) {
+  return Response.json({ error: 'Audio file too large. Maximum size is 25MB.' }, { status: 400 });
+}
+if (!audioFile.type.startsWith('audio/')) {
+  return Response.json({ error: 'Invalid file type. Only audio files are allowed.' }, { status: 400 });
+}
 const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
 await writeFile(tmpFilePath, buffer);
apps/registry/app/api/chat/route.js (1)

166-203: ⚠️ Potential issue

Implement more granular error handling.

Building upon the previous review comment, the error handling needs improvement.

     // Process tool calls
     for (const toolCall of response.tool_calls) {
       if (toolCall.function.name === 'update_resume') {
         try {
           const { changes, explanation } = JSON.parse(
             toolCall.function.arguments
           );
+          // Validate changes against schema
+          if (!changes || typeof changes !== 'object') {
+            throw new Error('Invalid changes format');
+          }
           return NextResponse.json({
             message: explanation,
             suggestedChanges: changes,
           });
         } catch (error) {
-          console.error('Error parsing tool call arguments:', error);
-          throw error;
+          console.error('Error processing tool call:', {
+            error,
+            toolCall,
+            function: toolCall.function.name
+          });
+          return NextResponse.json({
+            error: {
+              type: 'ToolCallError',
+              message: 'Failed to process AI suggestions',
+              details: process.env.NODE_ENV === 'development' ? error.message : undefined
+            }
+          }, { status: 422 });
         }
       }
     }
 
     // Fallback response if no relevant tool calls
     return NextResponse.json({
       message: response.content,
       suggestedChanges: null,
     });
   } catch (error) {
-    console.error('Error:', error);
-    return NextResponse.json({ error: error.message }, { status: 500 });
+    console.error('Error processing chat completion:', {
+      error,
+      messages,
+      currentResume
+    });
+    
+    let status = 500;
+    let message = 'Internal server error';
+    
+    if (error.name === 'OpenAIError') {
+      status = error.status || 500;
+      message = 'AI service error';
+    } else if (error instanceof SyntaxError) {
+      status = 400;
+      message = 'Invalid request format';
+    }
+    
+    return NextResponse.json({
+      error: {
+        type: error.name || 'UnknownError',
+        message,
+        details: process.env.NODE_ENV === 'development' ? error.stack : undefined
+      }
+    }, { status });
   }
🧹 Nitpick comments (2)
apps/registry/app/api/transcribe/route.js (1)

35-42: Consider making model and language configurable.

The Whisper model version and language are hardcoded. Consider making these configurable through environment variables or request parameters for more flexibility.

+const WHISPER_MODEL = process.env.WHISPER_MODEL || 'whisper-1';
+const DEFAULT_LANGUAGE = process.env.DEFAULT_LANGUAGE || 'en';
+
+// Extract language from request if provided
+const language = formData.get('language') || DEFAULT_LANGUAGE;
+
 const transcription = await openai.audio.transcriptions.create({
   file: fs.createReadStream(tmpFilePath),
-  model: 'whisper-1',
-  language: 'en',
+  model: WHISPER_MODEL,
+  language: language,
   response_format: 'text',
 });
apps/registry/app/api/chat/route.js (1)

4-18: Enhance system prompt with validation rules and complex examples.

The system prompt could be improved by:

  1. Including validation rules for specific fields (email format, phone format, URL validation)
  2. Adding examples for complex nested objects (location, profiles)
  3. Listing required fields for each section
 const systemPrompt = `You are an AI assistant helping to edit a JSON Resume (https://jsonresume.org/schema/).
 When suggesting changes:
 1. Return a JSON object containing ONLY the sections that need to be modified
 2. For array sections (work, education, etc.):
    - To ADD a new item: include it in the array
    - To UPDATE an existing item: include the full item with all changes
    - To DELETE an item: include it with a "_delete": true flag and enough identifying information (name, dates)
 3. Keep responses concise but friendly
 4. Always validate dates are in YYYY-MM-DD format
 5. Ensure all required fields are present
+6. Validate field formats:
+   - email: must be a valid email address
+   - phone: must follow E.164 format
+   - url: must be a valid URL
+7. Required fields by section:
+   - basics: name, email
+   - work: name, position, startDate
+   - education: institution, area, startDate
+   - skills: name

 Example responses:
 Adding: {"work": [{"name": "New Co", "position": "Engineer", "startDate": "2020-01-01"}]}
 Updating: {"work": [{"name": "Existing Co", "position": "Senior Engineer"}]}
-Deleting: {"work": [{"name": "Old Co", "_delete": true}]}`;
+Deleting: {"work": [{"name": "Old Co", "_delete": true}]}
+
+Complex examples:
+{"basics": {"location": {"address": "123 Main St", "city": "San Francisco", "region": "CA", "countryCode": "US"}}}
+{"basics": {"profiles": [{"network": "LinkedIn", "url": "https://linkedin.com/in/username"}]}}`;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2730a9 and 790031c.

📒 Files selected for processing (2)
  • apps/registry/app/api/chat/route.js (1 hunks)
  • apps/registry/app/api/transcribe/route.js (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Meticulous Tests

Comment on lines +57 to +66
} catch (error) {
console.error('Error transcribing audio:', error);
return Response.json(
{
error: 'Failed to transcribe audio',
details: error.message,
},
{ status: 500 }
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and logging.

The current error handling might expose sensitive information in the error details. Consider:

  1. Sanitizing error messages before sending them to the client
  2. Adding structured logging with error codes
  3. Including request correlation IDs for better debugging
+const sanitizeError = (error) => {
+  // Return a safe error message based on error type
+  if (error.code === 'ENOENT') return 'File processing error';
+  if (error.response?.status === 401) return 'Authentication error';
+  return 'Internal server error';
+};
+
 catch (error) {
-  console.error('Error transcribing audio:', error);
+  const correlationId = Date.now().toString(36);
+  console.error('Error transcribing audio:', {
+    correlationId,
+    error: error.message,
+    stack: error.stack,
+  });
   return Response.json(
     {
       error: 'Failed to transcribe audio',
-      details: error.message,
+      message: sanitizeError(error),
+      correlationId,
     },
     { status: 500 }
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error('Error transcribing audio:', error);
return Response.json(
{
error: 'Failed to transcribe audio',
details: error.message,
},
{ status: 500 }
);
}
+const sanitizeError = (error) => {
+ // Return a safe error message based on error type
+ if (error.code === 'ENOENT') return 'File processing error';
+ if (error.response?.status === 401) return 'Authentication error';
+ return 'Internal server error';
+};
} catch (error) {
+ const correlationId = Date.now().toString(36);
+ console.error('Error transcribing audio:', {
+ correlationId,
+ error: error.message,
+ stack: error.stack,
+ });
return Response.json(
{
error: 'Failed to transcribe audio',
+ message: sanitizeError(error),
+ correlationId,
- details: error.message,
},
{ status: 500 }
);
}

Comment on lines +29 to +34
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
await writeFile(tmpFilePath, buffer);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve temporary file handling.

The current implementation has potential issues:

  1. The .webm extension is hardcoded but the input file might be in a different format.
  2. The /tmp directory might not be available in all deployment environments.
-const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
+const extension = audioFile.name.split('.').pop() || 'webm';
+const tmpDir = process.env.TEMP_DIR || '/tmp';
+const tmpFilePath = join(tmpDir, `audio-${Date.now()}.${extension}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const tmpFilePath = join('/tmp', `audio-${Date.now()}.webm`);
await writeFile(tmpFilePath, buffer);
// Create a temporary file
const bytes = await audioFile.arrayBuffer();
const buffer = Buffer.from(bytes);
const extension = audioFile.name.split('.').pop() || 'webm';
const tmpDir = process.env.TEMP_DIR || '/tmp';
const tmpFilePath = join(tmpDir, `audio-${Date.now()}.${extension}`);
await writeFile(tmpFilePath, buffer);

Comment on lines +43 to +56
// Clean up the temporary file
fs.unlink(tmpFilePath, (err) => {
if (err) console.error('Error deleting temporary file:', err);
});

return Response.json({ text: transcription });
} finally {
// Ensure we try to clean up the temp file even if transcription fails
try {
fs.unlinkSync(tmpFilePath);
} catch (e) {
// Ignore errors during cleanup
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize temporary file cleanup.

The code attempts to delete the temporary file twice, which could lead to race conditions. Consider consolidating the cleanup into a single operation.

-// Clean up the temporary file
-fs.unlink(tmpFilePath, (err) => {
-  if (err) console.error('Error deleting temporary file:', err);
-});
-
 return Response.json({ text: transcription });
 } finally {
   // Ensure we try to clean up the temp file even if transcription fails
   try {
     fs.unlinkSync(tmpFilePath);
   } catch (e) {
-    // Ignore errors during cleanup
+    console.error('Error deleting temporary file:', e);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Clean up the temporary file
fs.unlink(tmpFilePath, (err) => {
if (err) console.error('Error deleting temporary file:', err);
});
return Response.json({ text: transcription });
} finally {
// Ensure we try to clean up the temp file even if transcription fails
try {
fs.unlinkSync(tmpFilePath);
} catch (e) {
// Ignore errors during cleanup
}
}
return Response.json({ text: transcription });
} finally {
// Ensure we try to clean up the temp file even if transcription fails
try {
fs.unlinkSync(tmpFilePath);
} catch (e) {
console.error('Error deleting temporary file:', e);
}
}

Comment on lines +32 to +165
try {
const { messages, currentResume } = await req.json();

const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'system',
content: `Current resume state: ${JSON.stringify(
currentResume,
null,
2
)}`,
},
...messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
],
tools: [
{
type: 'function',
function: {
name: 'update_resume',
description:
'Update specific sections of the resume with new information',
parameters: {
type: 'object',
properties: {
changes: {
type: 'object',
description: 'Changes to apply to the resume',
properties: {
basics: {
type: 'object',
properties: {
name: { type: 'string' },
label: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
url: { type: 'string' },
summary: { type: 'string' },
location: {
type: 'object',
properties: {
address: { type: 'string' },
postalCode: { type: 'string' },
city: { type: 'string' },
countryCode: { type: 'string' },
region: { type: 'string' },
},
},
profiles: {
type: 'array',
items: {
type: 'object',
properties: {
network: { type: 'string' },
username: { type: 'string' },
url: { type: 'string' },
},
},
},
},
},
work: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
position: { type: 'string' },
url: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
summary: { type: 'string' },
highlights: {
type: 'array',
items: { type: 'string' },
},
technologies: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
education: {
type: 'array',
items: {
type: 'object',
properties: {
institution: { type: 'string' },
area: { type: 'string' },
studyType: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
score: { type: 'string' },
_delete: { type: 'boolean' },
},
},
},
skills: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
level: { type: 'string' },
keywords: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
},
},
explanation: {
type: 'string',
description:
'A brief, friendly explanation of the changes being made',
},
},
required: ['changes', 'explanation'],
},
},
},
],
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation and security measures.

The current implementation needs:

  1. Request body validation
  2. Size limits for messages array
  3. Sanitization of user input before JSON.stringify
   try {
+    if (!req.body) {
+      return NextResponse.json(
+        { error: 'Missing request body' },
+        { status: 400 }
+      );
+    }
+
     const { messages, currentResume } = await req.json();
+
+    // Validate required fields
+    if (!messages || !Array.isArray(messages) || !currentResume) {
+      return NextResponse.json(
+        { error: 'Invalid request format' },
+        { status: 400 }
+      );
+    }
+
+    // Limit message history
+    if (messages.length > 50) {
+      return NextResponse.json(
+        { error: 'Too many messages' },
+        { status: 400 }
+      );
+    }
+
+    // Validate message format
+    if (!messages.every(msg => 
+      msg && 
+      typeof msg.role === 'string' && 
+      typeof msg.content === 'string' &&
+      msg.content.length < 4000
+    )) {
+      return NextResponse.json(
+        { error: 'Invalid message format' },
+        { status: 400 }
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const { messages, currentResume } = await req.json();
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'system',
content: `Current resume state: ${JSON.stringify(
currentResume,
null,
2
)}`,
},
...messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
],
tools: [
{
type: 'function',
function: {
name: 'update_resume',
description:
'Update specific sections of the resume with new information',
parameters: {
type: 'object',
properties: {
changes: {
type: 'object',
description: 'Changes to apply to the resume',
properties: {
basics: {
type: 'object',
properties: {
name: { type: 'string' },
label: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
url: { type: 'string' },
summary: { type: 'string' },
location: {
type: 'object',
properties: {
address: { type: 'string' },
postalCode: { type: 'string' },
city: { type: 'string' },
countryCode: { type: 'string' },
region: { type: 'string' },
},
},
profiles: {
type: 'array',
items: {
type: 'object',
properties: {
network: { type: 'string' },
username: { type: 'string' },
url: { type: 'string' },
},
},
},
},
},
work: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
position: { type: 'string' },
url: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
summary: { type: 'string' },
highlights: {
type: 'array',
items: { type: 'string' },
},
technologies: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
education: {
type: 'array',
items: {
type: 'object',
properties: {
institution: { type: 'string' },
area: { type: 'string' },
studyType: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
score: { type: 'string' },
_delete: { type: 'boolean' },
},
},
},
skills: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
level: { type: 'string' },
keywords: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
},
},
explanation: {
type: 'string',
description:
'A brief, friendly explanation of the changes being made',
},
},
required: ['changes', 'explanation'],
},
},
},
],
});
try {
if (!req.body) {
return NextResponse.json(
{ error: 'Missing request body' },
{ status: 400 }
);
}
const { messages, currentResume } = await req.json();
// Validate required fields
if (!messages || !Array.isArray(messages) || !currentResume) {
return NextResponse.json(
{ error: 'Invalid request format' },
{ status: 400 }
);
}
// Limit message history
if (messages.length > 50) {
return NextResponse.json(
{ error: 'Too many messages' },
{ status: 400 }
);
}
// Validate message format
if (
!messages.every(
(msg) =>
msg &&
typeof msg.role === 'string' &&
typeof msg.content === 'string' &&
msg.content.length < 4000
)
) {
return NextResponse.json(
{ error: 'Invalid message format' },
{ status: 400 }
);
}
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{ role: 'system', content: systemPrompt },
{
role: 'system',
content: `Current resume state: ${JSON.stringify(
currentResume,
null,
2
)}`,
},
...messages.map((msg) => ({
role: msg.role,
content: msg.content,
})),
],
tools: [
{
type: 'function',
function: {
name: 'update_resume',
description:
'Update specific sections of the resume with new information',
parameters: {
type: 'object',
properties: {
changes: {
type: 'object',
description: 'Changes to apply to the resume',
properties: {
basics: {
type: 'object',
properties: {
name: { type: 'string' },
label: { type: 'string' },
email: { type: 'string' },
phone: { type: 'string' },
url: { type: 'string' },
summary: { type: 'string' },
location: {
type: 'object',
properties: {
address: { type: 'string' },
postalCode: { type: 'string' },
city: { type: 'string' },
countryCode: { type: 'string' },
region: { type: 'string' },
},
},
profiles: {
type: 'array',
items: {
type: 'object',
properties: {
network: { type: 'string' },
username: { type: 'string' },
url: { type: 'string' },
},
},
},
},
},
work: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
position: { type: 'string' },
url: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
summary: { type: 'string' },
highlights: {
type: 'array',
items: { type: 'string' },
},
technologies: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
education: {
type: 'array',
items: {
type: 'object',
properties: {
institution: { type: 'string' },
area: { type: 'string' },
studyType: { type: 'string' },
startDate: { type: 'string' },
endDate: { type: 'string' },
score: { type: 'string' },
_delete: { type: 'boolean' },
},
},
},
skills: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
level: { type: 'string' },
keywords: {
type: 'array',
items: { type: 'string' },
},
_delete: { type: 'boolean' },
},
},
},
},
},
explanation: {
type: 'string',
description:
'A brief, friendly explanation of the changes being made',
},
},
required: ['changes', 'explanation'],
},
},
},
],
});

Comment on lines +20 to +31
export async function POST(req) {
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json(
{ message: 'API not available during build' },
{ status: 503 }
);
}

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add rate limiting and improve API key handling.

Consider implementing:

  1. Rate limiting to prevent abuse
  2. API key validation before initialization
  3. Timeout configuration for OpenAI client
+import rateLimit from 'express-rate-limit';
+
+const limiter = rateLimit({
+  windowMs: 15 * 60 * 1000, // 15 minutes
+  max: 100 // limit each IP to 100 requests per windowMs
+});

 export async function POST(req) {
   if (!process.env.OPENAI_API_KEY) {
     return NextResponse.json(
       { message: 'API not available during build' },
       { status: 503 }
     );
   }
 
+  // Validate API key format
+  if (!process.env.OPENAI_API_KEY.startsWith('sk-')) {
+    console.error('Invalid OpenAI API key format');
+    return NextResponse.json(
+      { error: 'Invalid API configuration' },
+      { status: 500 }
+    );
+  }
+
   const openai = new OpenAI({
     apiKey: process.env.OPENAI_API_KEY,
+    timeout: 30000, // 30 second timeout
+    maxRetries: 3
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function POST(req) {
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json(
{ message: 'API not available during build' },
{ status: 503 }
);
}
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
export async function POST(req) {
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json(
{ message: 'API not available during build' },
{ status: 503 }
);
}
// Validate API key format
if (!process.env.OPENAI_API_KEY.startsWith('sk-')) {
console.error('Invalid OpenAI API key format');
return NextResponse.json(
{ error: 'Invalid API configuration' },
{ status: 500 }
);
}
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
timeout: 30000, // 30 second timeout
maxRetries: 3
});
// ... rest of the handler logic
}

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants