Skip to content

Commit

Permalink
Add OpenAI API
Browse files Browse the repository at this point in the history
  • Loading branch information
YuheiNakasaka committed Oct 4, 2023
1 parent 847df21 commit c6197e4
Show file tree
Hide file tree
Showing 12 changed files with 427 additions and 17 deletions.
17 changes: 15 additions & 2 deletions app/features/bookmark/components/bookmark-card.tsx
Expand Up @@ -6,14 +6,24 @@ interface BookmarkCardProps {
url: string;
image: string | null;
comment: string | null;
description: string | null;
count: number;
editable: boolean;
createdAt: string;
}

export default function BookmarkCard(props: BookmarkCardProps) {
const { slug, title, url, image, comment, count, createdAt, editable } =
props;
const {
slug,
title,
description,
url,
image,
comment,
count,
createdAt,
editable,
} = props;
const date = new Date(createdAt);
const formattedDate = date.toLocaleDateString("ja-JP", {
year: "2-digit",
Expand All @@ -40,6 +50,9 @@ export default function BookmarkCard(props: BookmarkCardProps) {
</a>

<p className="mt-4 mb-4">{comment}</p>

{description && <small>{`${description}`.slice(0, 100)}...</small>}

<div className="flex justify-between">
{editable ? (
<a className="text-xs text-gray-500" href={`/bookmarks/${slug}`}>
Expand Down
1 change: 1 addition & 0 deletions app/features/bookmark/types/bookmark.ts
Expand Up @@ -3,6 +3,7 @@ export type Bookmark = {
slug: string;
url: string;
title: string | null;
description: string | null;
imageKey: string | null;
comment: string | null;
createdAt: Date;
Expand Down
53 changes: 50 additions & 3 deletions app/queue/consumer.ts
@@ -1,6 +1,7 @@
import { bookmarks } from "db/schema";
import { eq } from "drizzle-orm";
import { createClient } from "~/features/common/services/db.server";
import { OpenAI } from "openai";

interface QueueBody {
type: string;
Expand All @@ -13,9 +14,10 @@ export async function queue(batch: MessageBatch, env: Env): Promise<void> {

for (const message of batch.messages) {
const { url, slug } = message.body as QueueBody;
const { title, image } = await fetchOGP(url);
const { title, image, description } = await fetchOGP(url);
const summary = await generateSummary(env, description);
const uploadedImage = await uploadImage(env, image);
await updateBookmark(env, slug, title, uploadedImage);
await updateBookmark(env, slug, title, summary, uploadedImage);
}
}

Expand All @@ -24,6 +26,12 @@ const fetchOGP = async (url: string) => {
const text = await fetch(url).then((res) => res.text());
const titleMatched = text.match(/<title>(.*)<\/title>/);
let title = titleMatched ? titleMatched[1] : "";

const descriptionMatched = text.match(
/<meta\s+name=[\'"]description[\'"]\s+content=[\'"](.+?)[\'"]\s*\/>/
);
const description = descriptionMatched ? descriptionMatched[1] : "";

let imageMatched = text.match(
/<meta\s+(?:property=[\'"]?og:image[\'"]?\s+)?content=[\'"](https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))?[\'"]\s*[\/]*>/i
);
Expand All @@ -36,7 +44,7 @@ const fetchOGP = async (url: string) => {
image = imageMatched ? imageMatched[1] : "";
}

return { title, image };
return { title, image, description };
};

const uploadImage = async (env: Env, image: string): Promise<string | null> => {
Expand All @@ -58,15 +66,18 @@ const updateBookmark = async (
env: Env,
slug: string,
title: string,
description: string,
image: string | null
) => {
const db = createClient(env.DB);
const updateBookmark: {
title: string;
description: string;
imageKey?: string;
isProcessed: boolean;
} = {
title: title,
description: description,
isProcessed: true,
};
if (image) {
Expand All @@ -79,3 +90,39 @@ const updateBookmark = async (
.returning()
.get();
};

const generateSummary = async (
env: Env,
description: string
): Promise<string> => {
const openAIBaseUrl =
env.OPENAI_API_URL !== ""
? env.OPENAI_API_URL
: "https://api.openai.com/v1";
const openai = new OpenAI({
apiKey: env.OPENAI_API_KEY,
baseURL: openAIBaseUrl,
});

const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{
role: "system",
content:
"You will be provided with a sentence in Japanese, and your task is to translate it into English.",
},
{
role: "user",
content: `${description}`,
},
],
temperature: 0,
max_tokens: 256,
});
console.log(JSON.stringify(response));

return response.choices.length > 0
? `${response.choices[0].message.content}`
: "";
};
1 change: 1 addition & 0 deletions app/routes/bookmarks.$slug/route.tsx
Expand Up @@ -90,6 +90,7 @@ export default function BookmarkEdit() {
editable={false}
slug={bookmark.slug}
title={bookmark.title}
description={bookmark.description}
url={bookmark.url}
image={bookmark.imageKey}
comment={bookmark.comment}
Expand Down
1 change: 1 addition & 0 deletions app/routes/users.$userId/route.tsx
Expand Up @@ -96,6 +96,7 @@ export default function Index() {
editable={true}
slug={bookmark.slug}
title={bookmark.title}
description={bookmark.description}
url={bookmark.url}
image={bookmark.imageKey}
comment={bookmark.comment}
Expand Down
1 change: 1 addition & 0 deletions db/migrate/0001_young_sentinels.sql
@@ -0,0 +1 @@
ALTER TABLE bookmarks ADD `description` text;
147 changes: 147 additions & 0 deletions db/migrate/meta/0001_snapshot.json
@@ -0,0 +1,147 @@
{
"version": "5",
"dialect": "sqlite",
"id": "9dd5148d-e287-46e6-843a-2f4bbcf49946",
"prevId": "f38e8a15-9e7a-4fe2-9182-73251a7b4860",
"tables": {
"bookmarks": {
"name": "bookmarks",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"comment": {
"name": "comment",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"imageKey": {
"name": "imageKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"isProcessed": {
"name": "isProcessed",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"bookmarks_userId_url_unique": {
"name": "bookmarks_userId_url_unique",
"columns": [
"userId",
"url"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"googleProfileId": {
"name": "googleProfileId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"iconUrl": {
"name": "iconUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"displayName": {
"name": "displayName",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
7 changes: 7 additions & 0 deletions db/migrate/meta/_journal.json
Expand Up @@ -8,6 +8,13 @@
"when": 1692979941680,
"tag": "0000_polite_madame_web",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1696435122703,
"tag": "0001_young_sentinels",
"breakpoints": true
}
]
}
1 change: 1 addition & 0 deletions db/schema.ts
Expand Up @@ -16,6 +16,7 @@ export const bookmarks = sqliteTable(
userId: integer("userId").notNull(),
url: text("url").notNull(),
title: text("title"),
description: text("description"),
comment: text("comment"),
imageKey: text("imageKey"),
isProcessed: integer("isProcessed", { mode: "boolean" })
Expand Down

0 comments on commit c6197e4

Please sign in to comment.