feat: add summary using Anthropic API

This commit is contained in:
Riccardo
2024-09-20 02:52:16 +02:00
parent 1917b040eb
commit c0a669dc0a
8 changed files with 325 additions and 59 deletions

View File

@@ -36,4 +36,5 @@ VERCEL_GIT_REPO_ID=""
VERCEL_GIT_REPO_OWNER="" VERCEL_GIT_REPO_OWNER=""
VERCEL_GIT_REPO_SLUG="" VERCEL_GIT_REPO_SLUG=""
VERCEL_URL="" VERCEL_URL=""
ANALYZE="false" ANALYZE="false"
ANTHROPIC_API_KEY=""

3
.gitignore vendored
View File

@@ -16,6 +16,9 @@
# production # production
/build /build
# analyze
/analyze
# yarn # yarn
/.yarn /.yarn

View File

@@ -83,9 +83,11 @@ export async function GET(request: NextRequest) {
const validRankedNews = news.sort((a, b) => b.score - a.score); const validRankedNews = news.sort((a, b) => b.score - a.score);
const template = await NewsletterTemplate(validRankedNews);
const sent = await sender( const sent = await sender(
users.map(user => user.email), users.map(user => user.email),
NewsletterTemplate(validRankedNews) template
); );
if (!sent) { if (!sent) {

View File

@@ -1,9 +1,12 @@
import { getSayings } from '@utils/getSayings'; import { getSayings } from '@utils/getSayings';
import { summirize } from '@utils/summarize';
import { textTruncate } from '@utils/textTruncate'; import { textTruncate } from '@utils/textTruncate';
import { NewsType } from '@utils/validationSchemas'; import { NewsType } from '@utils/validationSchemas';
import Template from './Template'; import Template from './Template';
export default function NewsletterTemplate(stories: NewsType[]) { export default async function NewsletterTemplate(stories: NewsType[]) {
const summary = await summirize(stories);
return { return {
subject: `What's new from the Hackernews forum?`, subject: `What's new from the Hackernews forum?`,
template: ( template: (
@@ -11,72 +14,90 @@ export default function NewsletterTemplate(stories: NewsType[]) {
title={`Here is something title={`Here is something
${getSayings[Math.floor(Math.random() * getSayings.length)]}!`} ${getSayings[Math.floor(Math.random() * getSayings.length)]}!`}
body={ body={
<div> <>
{stories.map(story => { {summary && (
return ( <div
<div style={{
key={story.id} marginTop: '2rem',
style={{ marginBottom: '2rem',
marginTop: '2rem', borderRadius: '0.5rem',
marginBottom: '2rem', border: '2px solid #8230CC',
borderRadius: '0.5rem', backgroundColor: `white`,
border: '1px solid #e5e7eb', color: '#111827',
backgroundColor: `white`, boxShadow: '0 16px 32px 0 rgba(0, 0, 0, 0.05)'
color: '#111827', }}
boxShadow: '0 16px 32px 0 rgba(0, 0, 0, 0.05)' data-v0-t='card'
}} >
data-v0-t='card' <p>{summary}</p>
> </div>
)}
<div>
{stories.map(story => {
return (
<div <div
key={story.id}
style={{ style={{
display: 'flex', marginTop: '2rem',
flexDirection: 'column', marginBottom: '2rem',
gap: '0.375rem', borderRadius: '0.5rem',
paddingTop: '1.5rem', border: '1px solid #e5e7eb',
paddingLeft: '1.5rem', backgroundColor: `white`,
paddingRight: '1.5rem' color: '#111827',
boxShadow: '0 16px 32px 0 rgba(0, 0, 0, 0.05)'
}} }}
data-v0-t='card'
> >
<h3>{story.title}</h3>
<p style={{ fontSize: '1rem', fontStyle: 'italic' }}>
by {story.by}
</p>
</div>
{story.text && (
<div <div
style={{ style={{
display: 'flex',
flexDirection: 'column',
gap: '0.375rem',
paddingTop: '1.5rem',
paddingLeft: '1.5rem', paddingLeft: '1.5rem',
fontSize: '1rem',
paddingRight: '1.5rem' paddingRight: '1.5rem'
}} }}
> >
<p <h3>{story.title}</h3>
dangerouslySetInnerHTML={{ <p style={{ fontSize: '1rem', fontStyle: 'italic' }}>
__html: by {story.by}
story.text.length > 500 </p>
? textTruncate(story.text, 500) + '...' </div>
: story.text {story.text && (
<div
style={{
paddingLeft: '1.5rem',
fontSize: '1rem',
paddingRight: '1.5rem'
}} }}
/> >
</div> <p
)} dangerouslySetInnerHTML={{
{story.url && ( __html:
<div story.text.length > 500
style={{ ? textTruncate(story.text, 500) + '...'
paddingBottom: '1.5rem', : story.text
paddingLeft: '1.5rem', }}
paddingRight: '1.5rem', />
textAlign: 'right', </div>
fontWeight: 'bold' )}
}} {story.url && (
> <div
<a href={story.url}>Read more</a> style={{
</div> paddingBottom: '1.5rem',
)} paddingLeft: '1.5rem',
</div> paddingRight: '1.5rem',
); textAlign: 'right',
})} fontWeight: 'bold'
</div> }}
>
<a href={story.url}>Read more</a>
</div>
)}
</div>
);
})}
</div>
</>
} }
/> />
) )

View File

@@ -21,6 +21,7 @@
"prisma:reset": "npx prisma db push --force-reset" "prisma:reset": "npx prisma db push --force-reset"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.27.3",
"@hookform/resolvers": "^3.3.2", "@hookform/resolvers": "^3.3.2",
"@next/bundle-analyzer": "^14.2.5", "@next/bundle-analyzer": "^14.2.5",
"@prisma/client": "^5.6.0", "@prisma/client": "^5.6.0",

13
utils/anthropic.ts Normal file
View File

@@ -0,0 +1,13 @@
import Anthropic from '@anthropic-ai/sdk';
export async function message(text: string) {
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY
});
return anthropic.messages.create({
model: 'claude-3-5-sonnet-20240620',
max_tokens: 1024,
messages: [{ role: 'user', content: text }]
});
}

21
utils/summarize.ts Normal file
View File

@@ -0,0 +1,21 @@
import { message } from './anthropic';
import { NewsType } from './validationSchemas';
export async function summirize(news: NewsType[]) {
const newsInput = news
.map(n => `TITLE: ${n.title}, CONTENT: ${n.text}, LINK: ${n.url}`)
.join('\n\n');
const promptSetup =
"You are a tech journalist with a technology degree and background. Summarize the following list of posts from an online forum as a TL;DR (Too Long; Didn't Read) summary. Your summary should:\n\n1. Be 200-500 words long.\n\n2. Consist multiple phrases in one single paragraph, combining related news items where possible.\n\n3. Highlight the 2-3 most significant or impactful news items.\n\n4. Provide context within broader tech trends or developments.\n\n5. Use a tone that is informative and slightly enthusiastic, aimed at tech-savvy general readers.\n\n6. Group news items by themes or technology areas if applicable.\n\n7. Be formatted for HTML use, with links incorporated as follows: <a href='[LINK]'>[linked text]</a>\n\nFocus on conveying the key points and their potential impact on the tech landscape. Your response should consist of the summary only.\n\nThe news items are structured as follows:\n\nTITLE: <title>\n\nCONTENT: <content>\n\nLINK: <link>\n\nPlease summarize the following news:";
try {
const response = await message(promptSetup + newsInput);
const summary = response.content[0] as { text: string };
return summary.text;
} catch (error) {
console.error(error);
return;
}
}

206
yarn.lock
View File

@@ -12,6 +12,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@anthropic-ai/sdk@npm:^0.27.3":
version: 0.27.3
resolution: "@anthropic-ai/sdk@npm:0.27.3"
dependencies:
"@types/node": "npm:^18.11.18"
"@types/node-fetch": "npm:^2.6.4"
abort-controller: "npm:^3.0.0"
agentkeepalive: "npm:^4.2.1"
form-data-encoder: "npm:1.7.2"
formdata-node: "npm:^4.3.2"
node-fetch: "npm:^2.6.7"
checksum: f855a10a2602f69ecfa791f2022ca5b492cc30fafd065138e3a9e12082f0fbece12008782c921746e3aa67bd72afbe69c51cfe90c66638e05e241c1c2f64d346
languageName: node
linkType: hard
"@babel/code-frame@npm:^7.0.0": "@babel/code-frame@npm:^7.0.0":
version: 7.24.7 version: 7.24.7
resolution: "@babel/code-frame@npm:7.24.7" resolution: "@babel/code-frame@npm:7.24.7"
@@ -742,6 +757,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node-fetch@npm:^2.6.4":
version: 2.6.11
resolution: "@types/node-fetch@npm:2.6.11"
dependencies:
"@types/node": "npm:*"
form-data: "npm:^4.0.0"
checksum: c416df8f182ec3826278ea42557fda08f169a48a05e60722d9c8edd4e5b2076ae281c6b6601ad406035b7201f885b0257983b61c26b3f9eb0f41192a807b5de5
languageName: node
linkType: hard
"@types/node@npm:*":
version: 22.5.5
resolution: "@types/node@npm:22.5.5"
dependencies:
undici-types: "npm:~6.19.2"
checksum: 172d02c8e6d921699edcf559c28b3805616bd6481af1b3cb0299f89ad9a6f33b71050434c06ce7b503166054a26275344187c443f99f745d0b12601372452f19
languageName: node
linkType: hard
"@types/node@npm:^18.11.18":
version: 18.19.50
resolution: "@types/node@npm:18.19.50"
dependencies:
undici-types: "npm:~5.26.4"
checksum: d238bb877953fcecda830df140f8722b9ba9644ae63e810fe6fa40cab8285c42f9b34c9529f2144a6f8cfeee5b0ff7fefd9425261e41830157d6710d501b829d
languageName: node
linkType: hard
"@types/node@npm:^20": "@types/node@npm:^20":
version: 20.14.11 version: 20.14.11
resolution: "@types/node@npm:20.14.11" resolution: "@types/node@npm:20.14.11"
@@ -957,6 +1000,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"abort-controller@npm:^3.0.0":
version: 3.0.0
resolution: "abort-controller@npm:3.0.0"
dependencies:
event-target-shim: "npm:^5.0.0"
checksum: ed84af329f1828327798229578b4fe03a4dd2596ba304083ebd2252666bdc1d7647d66d0b18704477e1f8aa315f055944aa6e859afebd341f12d0a53c37b4b40
languageName: node
linkType: hard
"acorn-jsx@npm:^5.3.2": "acorn-jsx@npm:^5.3.2":
version: 5.3.2 version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2" resolution: "acorn-jsx@npm:5.3.2"
@@ -993,6 +1045,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"agentkeepalive@npm:^4.2.1":
version: 4.5.0
resolution: "agentkeepalive@npm:4.5.0"
dependencies:
humanize-ms: "npm:^1.2.1"
checksum: dd210ba2a2e2482028f027b1156789744aadbfd773a6c9dd8e4e8001930d5af82382abe19a69240307b1d8003222ce6b0542935038313434b900e351914fc15f
languageName: node
linkType: hard
"aggregate-error@npm:^3.0.0": "aggregate-error@npm:^3.0.0":
version: 3.1.0 version: 3.1.0
resolution: "aggregate-error@npm:3.1.0" resolution: "aggregate-error@npm:3.1.0"
@@ -1246,6 +1307,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 3ce727cbc78f69d6a4722517a58ee926c8c21083633b1d3fdf66fd688f6c127a53a592141bd4866f9b63240a86e9d8e974b13919450bd17fa33c2d22c4558ad8
languageName: node
linkType: hard
"audit-ci@npm:^6.6.1": "audit-ci@npm:^6.6.1":
version: 6.6.1 version: 6.6.1
resolution: "audit-ci@npm:6.6.1" resolution: "audit-ci@npm:6.6.1"
@@ -1604,6 +1672,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: "npm:~1.0.0"
checksum: 2e969e637d05d09fa50b02d74c83a1186f6914aae89e6653b62595cc75a221464f884f55f231b8f4df7a49537fba60bdc0427acd2bf324c09a1dbb84837e36e4
languageName: node
linkType: hard
"commander@npm:^10.0.0": "commander@npm:^10.0.0":
version: 10.0.1 version: 10.0.1
resolution: "commander@npm:10.0.1" resolution: "commander@npm:10.0.1"
@@ -1902,6 +1979,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020
languageName: node
linkType: hard
"didyoumean@npm:^1.2.2": "didyoumean@npm:^1.2.2":
version: 1.2.2 version: 1.2.2
resolution: "didyoumean@npm:1.2.2" resolution: "didyoumean@npm:1.2.2"
@@ -2553,6 +2637,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"event-target-shim@npm:^5.0.0":
version: 5.0.1
resolution: "event-target-shim@npm:5.0.1"
checksum: 49ff46c3a7facbad3decb31f597063e761785d7fdb3920d4989d7b08c97a61c2f51183e2f3a03130c9088df88d4b489b1b79ab632219901f184f85158508f4c8
languageName: node
linkType: hard
"eventemitter3@npm:^5.0.1": "eventemitter3@npm:^5.0.1":
version: 5.0.1 version: 5.0.1
resolution: "eventemitter3@npm:5.0.1" resolution: "eventemitter3@npm:5.0.1"
@@ -2733,6 +2824,34 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"form-data-encoder@npm:1.7.2":
version: 1.7.2
resolution: "form-data-encoder@npm:1.7.2"
checksum: 227bf2cea083284411fd67472ccc22f5cb354ca92c00690e11ff5ed942d993c13ac99dea365046306200f8bd71e1a7858d2d99e236de694b806b1f374a4ee341
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
version: 4.0.0
resolution: "form-data@npm:4.0.0"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
mime-types: "npm:^2.1.12"
checksum: 7264aa760a8cf09482816d8300f1b6e2423de1b02bba612a136857413fdc96d7178298ced106817655facc6b89036c6e12ae31c9eb5bdc16aabf502ae8a5d805
languageName: node
linkType: hard
"formdata-node@npm:^4.3.2":
version: 4.4.1
resolution: "formdata-node@npm:4.4.1"
dependencies:
node-domexception: "npm:1.0.0"
web-streams-polyfill: "npm:4.0.0-beta.3"
checksum: 29622f75533107c1bbcbe31fda683e6a55859af7f48ec354a9800591ce7947ed84cd3ef2b2fcb812047a884f17a1bac75ce098ffc17e23402cd373e49c1cd335
languageName: node
linkType: hard
"fraction.js@npm:^4.3.7": "fraction.js@npm:^4.3.7":
version: 4.3.7 version: 4.3.7
resolution: "fraction.js@npm:4.3.7" resolution: "fraction.js@npm:4.3.7"
@@ -3187,6 +3306,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"humanize-ms@npm:^1.2.1":
version: 1.2.1
resolution: "humanize-ms@npm:1.2.1"
dependencies:
ms: "npm:^2.0.0"
checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16
languageName: node
linkType: hard
"husky@npm:^8.0.3": "husky@npm:^8.0.3":
version: 8.0.3 version: 8.0.3
resolution: "husky@npm:8.0.3" resolution: "husky@npm:8.0.3"
@@ -4110,6 +4238,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 54bb60bf39e6f8689f6622784e668a3d7f8bed6b0d886f5c3c446cb3284be28b30bf707ed05d0fe44a036f8469976b2629bbea182684977b084de9da274694d7
languageName: node
linkType: hard
"mime-types@npm:^2.1.12":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: "npm:1.52.0"
checksum: 89aa9651b67644035de2784a6e665fc685d79aba61857e02b9c8758da874a754aed4a9aced9265f5ed1171fd934331e5516b84a7f0218031b6fa0270eca1e51a
languageName: node
linkType: hard
"mimic-fn@npm:^2.1.0": "mimic-fn@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "mimic-fn@npm:2.1.0" resolution: "mimic-fn@npm:2.1.0"
@@ -4292,7 +4436,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ms@npm:^2.1.1": "ms@npm:^2.0.0, ms@npm:^2.1.1":
version: 2.1.3 version: 2.1.3
resolution: "ms@npm:2.1.3" resolution: "ms@npm:2.1.3"
checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d
@@ -4395,6 +4539,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "nextjs-hackernews@workspace:." resolution: "nextjs-hackernews@workspace:."
dependencies: dependencies:
"@anthropic-ai/sdk": "npm:^0.27.3"
"@commitlint/cli": "npm:^18.4.3" "@commitlint/cli": "npm:^18.4.3"
"@commitlint/config-conventional": "npm:^18.4.3" "@commitlint/config-conventional": "npm:^18.4.3"
"@hookform/resolvers": "npm:^3.3.2" "@hookform/resolvers": "npm:^3.3.2"
@@ -4435,6 +4580,27 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"node-domexception@npm:1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
checksum: e332522f242348c511640c25a6fc7da4f30e09e580c70c6b13cb0be83c78c3e71c8d4665af2527e869fc96848924a4316ae7ec9014c091e2156f41739d4fa233
languageName: node
linkType: hard
"node-fetch@npm:^2.6.7":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676
languageName: node
linkType: hard
"node-gyp@npm:latest": "node-gyp@npm:latest":
version: 10.2.0 version: 10.2.0
resolution: "node-gyp@npm:10.2.0" resolution: "node-gyp@npm:10.2.0"
@@ -6130,6 +6296,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tr46@npm:~0.0.3":
version: 0.0.3
resolution: "tr46@npm:0.0.3"
checksum: 8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695
languageName: node
linkType: hard
"trim-newlines@npm:^3.0.0": "trim-newlines@npm:^3.0.0":
version: 3.0.1 version: 3.0.1
resolution: "trim-newlines@npm:3.0.1" resolution: "trim-newlines@npm:3.0.1"
@@ -6300,6 +6473,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"undici-types@npm:~6.19.2":
version: 6.19.8
resolution: "undici-types@npm:6.19.8"
checksum: cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70
languageName: node
linkType: hard
"unique-filename@npm:^3.0.0": "unique-filename@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "unique-filename@npm:3.0.0" resolution: "unique-filename@npm:3.0.0"
@@ -6358,6 +6538,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"web-streams-polyfill@npm:4.0.0-beta.3":
version: 4.0.0-beta.3
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
checksum: dcdef67de57d83008f9dc330662b65ba4497315555dd0e4e7bcacb132ffdf8a830eaab8f74ad40a4a44f542461f51223f406e2a446ece1cc29927859b1405853
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
checksum: b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad
languageName: node
linkType: hard
"webpack-bundle-analyzer@npm:4.10.1": "webpack-bundle-analyzer@npm:4.10.1":
version: 4.10.1 version: 4.10.1
resolution: "webpack-bundle-analyzer@npm:4.10.1" resolution: "webpack-bundle-analyzer@npm:4.10.1"
@@ -6381,6 +6575,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"whatwg-url@npm:^5.0.0":
version: 5.0.0
resolution: "whatwg-url@npm:5.0.0"
dependencies:
tr46: "npm:~0.0.3"
webidl-conversions: "npm:^3.0.0"
checksum: f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07
languageName: node
linkType: hard
"which-boxed-primitive@npm:^1.0.2": "which-boxed-primitive@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "which-boxed-primitive@npm:1.0.2" resolution: "which-boxed-primitive@npm:1.0.2"