OpenAI integration and Note generation from prompt

This commit is contained in:
Michael Dausmann
2023-04-29 00:00:52 +10:00
parent d00d048b72
commit 3e3c5e57d7
11 changed files with 171 additions and 19 deletions

View File

@@ -12,4 +12,6 @@ STRIPE_CALLBACK_URL=http://localhost:3000
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://postgres:xxxxxxxxxxxxx@db.xxxxxxxxxxxxx.supabase.co:5432/postgres"
DATABASE_URL="postgresql://postgres:xxxxxxxxxxxxx@db.xxxxxxxxxxxxx.supabase.co:5432/postgres"
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

View File

@@ -11,6 +11,7 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op
- Pinia (State Store. I liked vuex a lot, in particular explicit mutations but gotta go with the cool crowd)
- Stripe (Payments including Webhook integration)
- Tailwind + daisyUI (Tailwind because everybody is using it, daisyUI because I suck at tailwind)
- OpenAI (text completions with AI)
## Features
### User Management
@@ -71,6 +72,7 @@ Please don't hitch your wagon to this star just yet... I'm coding this in the op
- [x] Max Notes limit property on Plan
- [x] Max Notes enforced
- [x] Add, Delete notes on Dashboard
- [x] AI Note generation with OpenAI
### Testing
- [x] Manual test scenario for auth and sub workflows passing

View File

@@ -1,4 +1,5 @@
import prisma_client from '~~/prisma/prisma.client';
import { openai } from './openai.client';
export default class NotesService {
async getAllNotes() {
@@ -33,4 +34,20 @@ export default class NotesService {
async deleteNote(id: number) {
return prisma_client.note.delete({ where: { id } });
}
async generateAINoteFromPrompt(userPrompt: string) {
const prompt = `
Write an interesting short note about ${userPrompt}.
Restrict the note to a single paragraph.
`
const completion = await openai.createCompletion({
model: "text-davinci-003",
prompt,
temperature: 0.6,
stop: "\n\n",
max_tokens: 1000,
n: 1,
});
return completion.data.choices[0].text;
}
}

View File

@@ -0,0 +1,9 @@
import { Configuration, OpenAIApi } from "openai";
const config = useRuntimeConfig();
const configuration = new Configuration({
apiKey: config.openAIKey,
});
export const openai = new OpenAIApi(configuration);

View File

@@ -18,6 +18,7 @@ export default defineNuxtConfig({
subscriptionGraceDays: 3,
initialPlanName: 'Free Trial',
initialPlanActiveMonths: 1,
openAIKey: process.env.OPENAI_API_KEY,
public: {
debugMode: true,
siteRootUrl: 'http://localhost:3000',

110
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@trpc/server": "^10.9.0",
"daisyui": "^2.51.5",
"generate-password-ts": "^1.6.3",
"openai": "^3.2.1",
"pinia": "^2.0.30",
"stripe": "^11.12.0",
"superjson": "^1.12.2",
@@ -2914,6 +2915,11 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"dev": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -2955,6 +2961,14 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"dependencies": {
"follow-redirects": "^1.14.8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3595,6 +3609,17 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -4097,6 +4122,14 @@
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz",
"integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ=="
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -4720,7 +4753,6 @@
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
"type": "individual",
@@ -4736,6 +4768,19 @@
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@@ -6374,7 +6419,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"engines": {
"node": ">= 0.6"
}
@@ -6383,7 +6427,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -7161,6 +7204,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openai": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz",
"integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==",
"dependencies": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
}
},
"node_modules/ora": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ora/-/ora-6.1.2.tgz",
@@ -13417,6 +13469,11 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@@ -13436,6 +13493,14 @@
"postcss-value-parser": "^4.2.0"
}
},
"axios": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
"requires": {
"follow-redirects": "^1.14.8"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -13874,6 +13939,14 @@
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz",
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
@@ -14245,6 +14318,11 @@
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.2.tgz",
"integrity": "sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ=="
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -14731,8 +14809,17 @@
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"formdata-polyfill": {
"version": "4.0.10",
@@ -16003,14 +16090,12 @@
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": {
"mime-db": "1.52.0"
}
@@ -16633,6 +16718,15 @@
}
}
},
"openai": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz",
"integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==",
"requires": {
"axios": "^0.26.0",
"form-data": "^4.0.0"
}
},
"ora": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ora/-/ora-6.1.2.tgz",

View File

@@ -28,6 +28,7 @@
"@trpc/server": "^10.9.0",
"daisyui": "^2.51.5",
"generate-password-ts": "^1.6.3",
"openai": "^3.2.1",
"pinia": "^2.0.30",
"stripe": "^11.12.0",
"superjson": "^1.12.2",

View File

@@ -17,6 +17,11 @@
newNoteText.value = '';
}
async function genNote(){
const genNoteText = await notesStore.generateAINoteFromPrompt(newNoteText.value)
newNoteText.value = genNoteText;
}
onMounted(async () => {
await accountStore.init();
await notesStore.fetchNotesForCurrentUser();
@@ -28,13 +33,18 @@
<h2 class="text-4xl font-extrabold tracking-tight text-gray-900 sm:text-5xl md:text-6xl mb-4">Notes Dashboard</h2>
</div>
<div class="w-full max-w-md mb-8">
<div v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.READ_WRITE || activeMembership.access === ACCOUNT_ACCESS.ADMIN || activeMembership.access === ACCOUNT_ACCESS.OWNER)" class="flex flex-row">
<input v-model="newNoteText" type="text" class="w-full rounded-l-md py-2 px-4 border-gray-400 border-2 focus:outline-none focus:border-blue-500" placeholder="Add a note...">
<div v-if="activeMembership && (activeMembership.access === ACCOUNT_ACCESS.READ_WRITE || activeMembership.access === ACCOUNT_ACCESS.ADMIN || activeMembership.access === ACCOUNT_ACCESS.OWNER)" class="w-full max-w-md mx-auto mb-3">
<textarea v-model="newNoteText" type="text" class="w-full rounded-l-md py-2 px-4 border-gray-400 border-2 focus:outline-none focus:border-blue-500" rows="5" placeholder="Add a note..."/>
<div class="flex justify-evenly">
<button @click.prevent="addNote()" type="button"
class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-r-md focus:outline-none focus:shadow-outline-blue">
class="px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
Add
</button>
<button @click.prevent="genNote()" type="button"
class="px-4 py-2 text-white bg-orange-500 rounded-md hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-opacity-50">
Gen
<Icon name="mdi:magic" class="h-6 w-6"/>
</button>
</div>
</div>

View File

@@ -54,7 +54,7 @@ const user = useSupabaseUser()
<li>
<Icon name="skill-icons:postgresql-dark" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">PostgreSQL</h3>
<p class="mt-2 text-base text-gray-500">The Progressive JavaScript Framework</p>
<p class="mt-2 text-base text-gray-500">Relational Database</p>
</li>
<li>
<Icon name="logos:prisma" class="h-12 w-12 mb-2" />
@@ -86,6 +86,11 @@ const user = useSupabaseUser()
<h3 class="text-xl font-medium text-gray-900">Vue.js</h3>
<p class="mt-2 text-base text-gray-500">The Progressive JavaScript Framework</p>
</li>
<li>
<Icon name="logos:openai-icon" class="h-12 w-12 mb-2" />
<h3 class="text-xl font-medium text-gray-900">OpenAI</h3>
<p class="mt-2 text-base text-gray-500">AI Completions including Note generation from prompt</p>
</li>
</ul>
</div>
</div>

View File

@@ -38,4 +38,13 @@ export const notesRouter = router({
note,
}
}),
generateAINoteFromPrompt: readWriteProcedure
.input(z.object({ user_prompt: z.string() }))
.query(async ({ ctx, input }) => {
const notesService = new NotesService();
const noteText = (ctx.activeAccountId)?await notesService.generateAINoteFromPrompt(input.user_prompt):null;
return {
noteText
}
}),
})

View File

@@ -2,10 +2,6 @@ import { Note } from ".prisma/client"
import { defineStore, storeToRefs } from "pinia"
import { Ref } from "vue";
interface State {
notes: Note[]
}
/*
Note) the Notes Store needs to be a 'Setup Store' (https://pinia.vuejs.org/core-concepts/#setup-stores)
because this enables the use of the watch on the Account Store
@@ -42,10 +38,16 @@ export const useNotesStore = defineStore('notes', () => {
}
}
async function generateAINoteFromPrompt(user_prompt: string) {
const { $client } = useNuxtApp();
const { noteText } = await $client.notes.generateAINoteFromPrompt.query({user_prompt});
return noteText?noteText:'';
}
// if the active account changes, fetch notes again (i.e dynamic.. probabl overkill)
watch(activeAccountId, async (val, oldVal)=> {
await fetchNotesForCurrentUser()
});
return { notes: _notes, fetchNotesForCurrentUser, createNote, deleteNote }
return { notes: _notes, fetchNotesForCurrentUser, createNote, deleteNote, generateAINoteFromPrompt}
});