Skip to content

Commit

Permalink
Add image gen with dall-e + update styles of home and explore pages
Browse files Browse the repository at this point in the history
  • Loading branch information
scottquested committed Jan 26, 2024
1 parent 76c9ea6 commit 442f7ea
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 76 deletions.
4 changes: 4 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import type {
} from "convex/server";
import type * as actionsNode from "../actionsNode.js";
import type * as mutations from "../mutations.js";
import type * as mutationsInternal from "../mutationsInternal.js";
import type * as queries from "../queries.js";
import type * as utils from "../utils.js";

/**
* A utility for referencing Convex functions in your app's API.
Expand All @@ -29,7 +31,9 @@ import type * as queries from "../queries.js";
declare const fullApi: ApiFromModules<{
actionsNode: typeof actionsNode;
mutations: typeof mutations;
mutationsInternal: typeof mutationsInternal;
queries: typeof queries;
utils: typeof utils;
}>;
export declare const api: FilterApi<
typeof fullApi,
Expand Down
Empty file added convex/actions.ts
Empty file.
72 changes: 64 additions & 8 deletions convex/actionsNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@

import { v } from "convex/values";
import { internalAction } from "./_generated/server";
import OpenAI from "openai";
import { internal } from "./_generated/api";

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
import { openAiConfig } from "./utils";

const userPromoptText = `What is considered the top job someone could
get with these skills. Don't include the question or skills in the response.
Expand All @@ -17,12 +13,21 @@ const systemPromoptText = `You are a helpful assistant who will return JSON data
for the top job someone could get with these skills. Use the keys 'jobTitle' and
'jobDescription' to return the data.`;

export const getOpenAiAnswer = internalAction({
/**
* Internal action to generate a job from given skills from OpenAi.
*
* @param skills The skills to use to generate the job.
* @param id The id of the job to update.
*
*/
export const getJobBySkills = internalAction({
args: {
skills: v.string(),
id: v.id("jobs"),
},
handler: async (ctx, { id, skills }) => {
const openai = await openAiConfig(skills);

try {
const completion = await openai.chat.completions.create({
messages: [
Expand All @@ -43,16 +48,67 @@ export const getOpenAiAnswer = internalAction({
completion.choices[0].message.content || "{}"
);

await ctx.runMutation(internal.mutations.updateJob, {
const storageId = await ctx.runAction(
internal.actionsNode.getOpenAiImage,
{
prompt: transformResult.jobTitle,
id,
}
);

await ctx.runMutation(internal.mutationsInternal.updateJob, {
id,
result: JSON.stringify(transformResult),
status: "completed",
imageId: storageId,
});
} catch (error) {
await ctx.runMutation(internal.mutations.updateJob, {
await ctx.runMutation(internal.mutationsInternal.updateJob, {
id,
status: "failed",
});
}
},
});

/**
* Internal action to get an image from OpenAI.
*
* @param prompt The prompt to use to generate the image.
* @returns The storage id of the image.
*
*/
export const getOpenAiImage = internalAction({
args: {
id: v.id("jobs"),
prompt: v.string(),
},
handler: async (ctx, { id, prompt }) => {
const openai = await openAiConfig(prompt);

console.log("prompt: ", prompt);

try {
// Query OpenAI for the image.
const opanaiResponse = await openai.images.generate({
prompt: `Image of ${prompt}. Make it a fun image`,
size: "512x512",
});
const dallEImageUrl = opanaiResponse.data[0]["url"]!;

// Download the image
const imageResponse = await fetch(dallEImageUrl);
if (!imageResponse.ok) {
throw new Error(`failed to download: ${imageResponse.statusText}`);
}

// Store the image to Convex storage.
const image = await imageResponse.blob();
const storageId = await ctx.storage.store(image as Blob);

return storageId;
} catch (error) {
throw new Error("Failed to get image");
}
},
});
25 changes: 4 additions & 21 deletions convex/mutations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { v } from "convex/values";
import { internal } from "./_generated/api";
import { internalMutation, mutation } from "./_generated/server";
import { mutation } from "./_generated/server";

export const saveJobs = mutation({
args: { skills: v.string(), userId: v.string() },
Expand All @@ -14,9 +14,10 @@ export const saveJobs = mutation({
userId,
skills,
status: "pending",
imageId: "",
});

await ctx.scheduler.runAfter(0, internal.actionsNode.getOpenAiAnswer, {
await ctx.scheduler.runAfter(0, internal.actionsNode.getJobBySkills, {
id: jobs,
skills,
});
Expand All @@ -41,27 +42,9 @@ export const retryJob = mutation({
status: "pending",
});

await ctx.scheduler.runAfter(0, internal.actionsNode.getOpenAiAnswer, {
await ctx.scheduler.runAfter(0, internal.actionsNode.getJobBySkills, {
id,
skills,
});
},
});

export const updateJob = internalMutation({
args: {
id: v.id("jobs"),
result: v.optional(v.string()),
status: v.union(
v.literal("pending"),
v.literal("completed"),
v.literal("failed")
),
},
handler: async (ctx, { id, result, status }) => {
await ctx.db.patch(id, {
result,
status: status,
});
},
});
22 changes: 22 additions & 0 deletions convex/mutationsInternal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { v } from "convex/values";
import { internalMutation } from "./_generated/server";

export const updateJob = internalMutation({
args: {
id: v.id("jobs"),
result: v.optional(v.string()),
status: v.union(
v.literal("pending"),
v.literal("completed"),
v.literal("failed")
),
imageId: v.optional(v.string()),
},
handler: async (ctx, { id, result, status, imageId }) => {
await ctx.db.patch(id, {
result,
status: status,
imageId,
});
},
});
1 change: 1 addition & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default defineSchema({
v.literal("completed"),
v.literal("failed")
),
imageId: v.string(),
}),
});
29 changes: 29 additions & 0 deletions convex/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import OpenAI from "openai";

export const openAiConfig = async (prompt: string) => {
const apiKey = process.env.OPENAI_API_KEY;

if (!apiKey) {
throw new Error(
"Add your OPENAI_API_KEY as an env variable in the " +
"[dashboard](https://dasboard.convex.dev)"
);
}

const openai = new OpenAI({ apiKey });

// Check if the prompt is offensive.
const modResponse = await openai.moderations.create({
input: prompt,
});
const modResult = modResponse.results[0];
if (modResult.flagged) {
throw new Error(
`Your prompt were flagged as offensive: ${JSON.stringify(
modResult.categories
)}`
);
}

return openai;
};
10 changes: 9 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
const nextConfig = {
images: {
remotePatterns: [
{
hostname: process.env.NEXT_PUBLIC_CONVEX_DOMAIN,
},
],
},
};

export default nextConfig;
14 changes: 11 additions & 3 deletions src/app/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import Jobs from "@/components/Jobs";
export default function Explore() {
return (
<main className="h-svh pt-24 container ">
<section className="pt-24">
<div className="max-w-screen-xl px-4 lg:gap-8 xl:gap-0 lg:py-16">
<Jobs />
<section>
<div className="max-w-screen-xl px-4 lg:gap-8 xl:gap-0 py-16">
<div className="mr-auto place-self-center">
<h1 className="max-w-2xl mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl xl:text-6xl dark:text-white">
Find your perfect job with just a few skills
</h1>
<p className="max-w-2xl mb-6 font-light text-gray-500 lg:mb-8 md:text-lg lg:text-xl dark:text-gray-400">
Input your skills and we&apos;ll find the perfect job for you.
</p>
<Jobs />
</div>
</div>
</section>
</main>
Expand Down
22 changes: 8 additions & 14 deletions src/app/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,21 @@ export default function Header() {
<header className="flex justify-between items-center p-4 border-b-2 border-gray-100 dark:border-gray-800 fixed z-50 w-full bg-background dark:text-white">
<Link
href="/"
className="font-bold text-xl flex items-center justify-center gap-2"
className="font-bold text-xl flex items-center justify-center gap-2 cursor-pointer"
>
<Briefcase className="text-orange-500" /> What&apos;s my job?
</Link>
<div className="flex gap-4">
<Link href="/explore">Explore</Link>
<Link href="/create">Create</Link>
<Link href="/explore" className="cursor-pointer">
Explore
</Link>
<Link href="/create" className="cursor-pointer">
Create
</Link>
</div>
<div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-3 cursor-pointer">
<SignedIn>
{!path.includes("dashboard") && (
<Link
href="/dashboard"
className={cn(
buttonVariants({ variant: "default", size: "sm" })
)}
>
Dashboard
</Link>
)}
<UserButton afterSignOutUrl="\" />
</SignedIn>
<SignedOut>
Expand Down
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export default function Home() {
return (
<main className="h-svh container">
<section className="pt-24">
<div className="grid max-w-screen-xl px-4 mx-auto lg:gap-8 xl:gap-0 lg:py-16 lg:grid-cols-12">
<div className="mr-auto place-self-center lg:col-span-7">
<div className="max-w-screen-xl px-4 lg:gap-8 xl:gap-0 py-16">
<div className="mr-auto place-self-center">
<h1 className="max-w-2xl mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl xl:text-6xl dark:text-white">
Find your perfect job with just a few skills
</h1>
Expand Down
64 changes: 40 additions & 24 deletions src/components/JobCard/JobCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,53 @@ import JobListFailed from "../JobListFailed";
import { Card, CardHeader, CardContent } from "../ui/card";
import { JobCardProps } from "./JobCard.types";
import { Badge } from "../ui/badge";
import Image from "next/image";
import { getImageUrl } from "@/lib/utils";

export default function JobCard({ job, result }: JobCardProps) {
return (
<Card className="h-full">
<Card className="h-full relative z-0">
{job.status === "pending" ? (
<div className="flex items-center justify-center h-full w-full">
<Loader2 className="animate-spin h-100 w-100 m-auto" />
Generating top job based on skills...
<div className="flex flex-col items-center justify-center h-full w-full">
<div>
<Loader2 className="animate-spin h-100 w-100 m-auto" />
Generating top job based on skills...
</div>
</div>
) : (
<>
<CardHeader className=" !mb-0">
<h1 className="text-xl font-semibold">{result?.jobTitle || ""}</h1>
</CardHeader>
<CardContent>
{!!job.skills.length && (
<div className="mb-4 flex gap-2">
{job.skills.split(",").map((skill) => (
<Badge variant="outline" key={skill}>
{skill}
</Badge>
))}
</div>
)}
{job.status === "failed" && <JobListFailed job={job} />}
{job.status === "completed" && (
<div>
<p>{result?.jobDescription || ""}</p>
</div>
)}
</CardContent>
<Image
src={getImageUrl(job.imageId)}
alt={`An image of a ${result.jobTitle || "job"}`}
width={400}
height={300}
className="w-full h-full object-cover object-top absolute top-0 left-0 z-0"
/>
<div className="w-full h-full object-cover object-top absolute top-0 left-0 z-10 bg-black opacity-70"></div>
<div className="relative z-10">
<CardHeader className="!mb-0">
<h1 className="text-xl font-semibold">
{result?.jobTitle || ""}
</h1>
</CardHeader>
<CardContent>
{!!job.skills.length && (
<div className="mb-4 flex gap-2">
{job.skills.split(",").map((skill) => (
<Badge variant="default" key={skill}>
{skill}
</Badge>
))}
</div>
)}
{job.status === "failed" && <JobListFailed job={job} />}
{job.status === "completed" && (
<div>
<p>{result?.jobDescription || ""}</p>
</div>
)}
</CardContent>
</div>
</>
)}
</Card>
Expand Down
Loading

0 comments on commit 442f7ea

Please sign in to comment.