Last active
April 7, 2024 05:00
-
-
Save eugene-yaroslavtsev/bcbc6c52b0f9662b1634f9a0f87e4d27 to your computer and use it in GitHub Desktop.
ChatGPT prompt sharing app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
schema.prisma | |
``` | |
datasource db { | |
provider = "postgresql" | |
url = env("DATABASE_URL") | |
} | |
generator client { | |
provider = "prisma-client-js" | |
} | |
model User { | |
id String @id @default(uuid()) | |
createdAt DateTime @default(now()) | |
email String @unique | |
entries Entry[] | |
} | |
model Entry { | |
id String @id @default(uuid()) | |
createdAt DateTime @default(now()) | |
userId String | |
user User @relation(fields: [userId], references: [id]) | |
prompt String | |
templateVariables Json | |
versions Version[] | |
likes Int @default(0) | |
dislikes Int @default(0) | |
} | |
model Version { | |
id String @id @default(uuid()) | |
createdAt DateTime @default(now()) | |
entryId String | |
entry Entry @relation(fields: [entryId], references: [id]) | |
responseText String | |
} | |
``` | |
src/main.ts | |
import { NestFactory } from '@nestjs/core'; | |
import { AppModule } from './app.module'; | |
async function bootstrap() { | |
const app = await NestFactory.create(AppModule); | |
await app.listen(3001); | |
} | |
bootstrap(); | |
src/app.module.ts | |
import { Module } from '@nestjs/common'; | |
import { ConfigModule } from '@nestjs/config'; | |
import { PrismaModule } from './prisma/prisma.module'; | |
import { EntriesModule } from './entries/entries.module'; | |
@Module({ | |
imports: [ | |
ConfigModule.forRoot(), | |
PrismaModule, | |
EntriesModule, | |
], | |
}) | |
export class AppModule {} | |
src/prisma/prisma.service.ts | |
import { PrismaClient } from '@prisma/client'; | |
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; | |
@Injectable() | |
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { | |
async onModuleInit() { | |
await this.$connect(); | |
} | |
async onModuleDestroy() { | |
await this.$disconnect(); | |
} | |
} | |
src/prisma/prisma.module.ts | |
import { Module } from '@nestjs/common' | |
import { PrismaService } from './prisma.service'; | |
@Module({ | |
providers: [PrismaService], | |
exports: [PrismaService], | |
}) | |
export class PrismaModule {} | |
src/entries/entries.controller.ts | |
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; | |
import { EntriesService } from './entries.service'; | |
import { CreateEntryDto } from './dto/create-entry.dto'; | |
import { UpdateEntryDto } from './dto/update-entry.dto'; | |
import { Entry } from './entities/entry.entity'; | |
@Controller('entries') | |
export class EntriesController { | |
constructor( | |
private readonly entriesService: EntriesService, | |
private readonly chatgptService: ChatgptService, | |
) {} | |
@Post() | |
create(@Body() createEntryDto: CreateEntryDto) { | |
return this.entriesService.create(createEntryDto); | |
} | |
@Get() | |
findAll() { | |
return this.entriesService.findAll(); | |
} | |
@Get(':id') | |
findOne(@Param('id') id: string) { | |
return this.entriesService.findOne(id); | |
} | |
@Patch(':id') | |
update(@Param('id') id: string, @Body() updateEntryDto: UpdateEntryDto) { | |
return this.entriesService.update(id, updateEntryDto); | |
} | |
@Delete(':id') | |
remove(@Param('id') id: string) { | |
return this.entriesService.remove(id); | |
} | |
@Post(':id/versions') | |
async createVersion(@Param('id') entryId: string): Promise<Entry> { | |
const entry = await this.entriesService.findOne(entryId); | |
const promptWithVars = await this.entriesService.interpolatePrompt(entry); | |
const response = await this.chatgptService.sendPrompt(promptWithVars); | |
return this.entriesService.createVersion(entryId, response); | |
} | |
@Patch(':id/like') | |
async likeEntry(@Param('id') entryId: string): Promise<Entry> { | |
return this.entriesService.likeEntry(entryId); | |
} | |
@Patch(':id/dislike') | |
async dislikeEntry(@Param('id') entryId: string): Promise<Entry> { | |
return this.entriesService.dislikeEntry(entryId); | |
} | |
} | |
src/entries/entries.module.ts | |
import { Module } from '@nestjs/common' | |
import { EntriesService } from './entries.service'; | |
import { EntriesController } from './entries.controller'; | |
import { PrismaModule } from '../prisma/prisma.module'; | |
import { ChatgptModule } from '../chatgpt/chatgpt.module'; | |
@Module({ | |
imports: [PrismaModule, ChatgptModule], | |
controllers: [EntriesController], | |
providers: [EntriesService], | |
}) | |
export class EntriesModule {} | |
import { Injectable } from '@nestjs/common'; | |
import { PrismaService } from '../prisma/prisma.service'; | |
import { CreateEntryDto } from './dto/create-entry.dto'; | |
import { UpdateEntryDto } from './dto/update-entry.dto'; | |
import { Entry } from './entities/entry.entity'; | |
@Injectable() | |
export class EntriesService { | |
constructor(private prisma: PrismaService) {} | |
// ... all other methods from previous responses | |
async interpolatePrompt(entry: Entry): Promise<string> { | |
const templateVars = JSON.parse(entry.templateVariables); | |
let promptWithVars = entry.prompt; | |
for (const key in templateVars) { | |
const regex = new RegExp(`\\$${key}`, 'g'); | |
promptWithVars = promptWithVars.replace(regex, templateVars[key]); | |
} | |
return promptWithVars; | |
} | |
} | |
src/chatgpt/chatgpt.module.ts | |
import { Module } from '@nestjs/common'; | |
import { ChatgptService } from './chatgpt.service'; | |
@Module({ | |
providers: [ChatgptService], | |
exports: [ChatgptService], | |
}) | |
export class ChatgptModule {} | |
src/chatgpt/chatgpt.service.ts | |
import { Injectable } from '@nestjs/common'; | |
import { ConfigService } from '@nestjs/config'; | |
import axios from 'axios'; | |
@Injectable() | |
export class ChatgptService { | |
constructor(private configService: ConfigService) {} | |
async sendPrompt(prompt: string): Promise<string> { | |
const response = await axios.post( | |
'https://api.openai.com/v1/engines/davinci-codex/completions', | |
{ | |
prompt, | |
max_tokens: 100, | |
n: 1, | |
stop: null, | |
temperature: 0.7, | |
}, | |
{ | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${this.configService.get('OPENAI_API_KEY')}`, | |
}, | |
}, | |
); | |
return response.data.choices[0].text.trim(); | |
} | |
} | |
src/index.tsx | |
import React from 'react'; | |
import ReactDOM from 'react-dom'; | |
import App from './App'; | |
import reportWebVitals from './reportWebVitals'; | |
ReactDOM.render( | |
<React.StrictMode> | |
<App /> | |
</React.StrictMode>, | |
document.getElementById('root'), | |
); | |
reportWebVitals(); | |
src/App.tsx | |
import React, { useState, useEffect } from 'react'; | |
import axios from 'axios'; | |
import EntryList from './components/EntryList'; | |
import EntryCreator from './components/EntryCreator'; | |
import SingleEntryView from './components/SingleEntryView'; | |
import VersionCreator from './components/VersionCreator'; | |
import { Entry } from './models/entry.model'; | |
import { Box } from '@mui/material'; | |
const apiUrl = 'http://localhost:3001/entries'; | |
function App() { | |
const [entries, setEntries] = useState<Entry[]>([]); | |
const [selectedEntry, setSelectedEntry] = useState<Entry | null>(null); | |
const fetchEntries = async () => { | |
const response = await axios.get<Entry[]>(apiUrl); | |
setEntries(response.data); | |
}; | |
useEffect(() => { | |
fetchEntries(); | |
}, []); | |
const handleCreateEntry = async (entry: Entry) => { | |
await axios.post(apiUrl, entry); | |
fetchEntries(); | |
}; | |
const handleCreateVersion = async () => { | |
await axios.post(`${apiUrl}/${selectedEntry.id}/versions`); | |
setSelectedEntry(null); | |
fetchEntries(); | |
}; | |
return ( | |
<Box sx={{ flexGrow: 1, p: 2 }}> | |
<EntryCreator onCreateEntry={handleCreateEntry} /> | |
<EntryList entries={entries} onSelectEntry={setSelectedEntry} /> | |
{selectedEntry && ( | |
<Box sx={{ mt: 2 }}> | |
<SingleEntryView entry={selectedEntry} /> | |
<Box sx={{ mt: 2 }}> | |
<VersionCreator entry={selectedEntry} onCreateVersion={handleCreateVersion} /> | |
</Box> | |
</Box> | |
)} | |
</Box> | |
); | |
} | |
export default App; | |
src/components/EntryList.tsx | |
import React from 'react'; | |
import { Entry } from '../models/entry.model'; | |
import { List, ListItem, ListItemText } from '@mui/material'; | |
interface EntryListProps { | |
entries: Entry[]; | |
onSelectEntry: (entry: Entry) => void; | |
} | |
const EntryList: React.FC<EntryListProps> = ({ entries, onSelectEntry }) => { | |
return ( | |
<List> | |
{entries.map((entry) => ( | |
<ListItem button key={entry.id} onClick={() => onSelectEntry(entry)}> | |
<ListItemText primary={entry.prompt} /> | |
</ListItem> | |
))} | |
</List> | |
); | |
}; | |
export default EntryList; | |
src/components/EntryCreator.tsx | |
import React, { useState } from 'react'; | |
import { Entry } from '../models/entry.model'; | |
import { TextField, Button, Box } from '@mui/material'; | |
interface EntryCreatorProps { | |
onCreateEntry: (entry: Entry) => void; | |
} | |
const EntryCreator: React.FC<EntryCreatorProps> = ({ onCreateEntry }) => { | |
const [prompt, setPrompt] = useState(''); | |
const [templateVariables, setTemplateVariables] = useState<Record<string, string>>({}); | |
const handleCreateEntry = () => { | |
onCreateEntry({ prompt, templateVariables: JSON.stringify(templateVariables) }); | |
setPrompt(''); | |
setTemplateVariables({}); | |
}; | |
const handleVariableChange = (key: string, value: string) => { | |
setTemplateVariables((prevState) => ({ ...prevState, [key]: value })); | |
}; | |
const variableInputs = Object.entries(templateVariables).map(([key, value]) => ( | |
<TextField | |
key={key} | |
label={key} | |
value={value} | |
onChange={(e) => handleVariableChange(key, e.target.value)} | |
sx={{ mt: 1 }} | |
/> | |
)); | |
const handleAddVariable = () => { | |
const key = `VAR_${Object.keys(templateVariables).length + 1}`; | |
setTemplateVariables((prevState) => ({ ...prevState, [key]: '' })); | |
}; | |
return ( | |
<Box sx={{ display: 'flex', flexDirection: 'column' }}> | |
<TextField | |
label="Prompt" | |
value={prompt} | |
onChange={(e) => setPrompt(e.target.value)} | |
fullWidth | |
/> | |
{variableInputs} | |
<Button variant="outlined" onClick={handleAddVariable} sx={{ mt: 1 }}> | |
Add Variable | |
</Button> | |
<Button variant="contained" onClick={handleCreateEntry} sx={{ mt: 1 }}> | |
Create Entry | |
</Button> | |
</Box> | |
); | |
}; | |
export default EntryCreator; | |
src/components/SingleEntryView.tsx | |
import React from 'react'; | |
import { Entry } from '../models/entry.model'; | |
import { Box, Typography } from '@mui/material'; | |
import ReactMarkdown from 'react-markdown'; | |
interface SingleEntryViewProps { | |
entry: Entry; | |
} | |
const SingleEntryView: React.FC<SingleEntryViewProps> = ({ entry }) => { | |
const latestVersion = entry.versions[entry.versions.length - 1]; | |
return ( | |
<Box> | |
<Typography variant="h5" gutterBottom> | |
{entry.prompt} | |
</Typography> | |
<ReactMarkdown>{latestVersion.responseText}</ReactMarkdown> | |
</Box> | |
); | |
}; | |
export default SingleEntryView; | |
src/components/VersionCreator.tsx | |
import React, { useState } from 'react'; | |
import { Entry } from '../models/entry.model'; | |
import { Button, CircularProgress, Box } from '@mui/material'; | |
interface VersionCreatorProps { | |
entry: Entry; | |
onCreateVersion: () => void; | |
} | |
const VersionCreator: React.FC<VersionCreatorProps> = ({ entry, onCreateVersion }) => { | |
const [isLoading, setIsLoading] = useState(false); | |
const handleClick = async () => { | |
setIsLoading(true); | |
await onCreateVersion(); | |
setIsLoading(false); | |
}; | |
return ( | |
<Box sx={{ display: 'flex', alignItems: 'center' }}> | |
<Button variant="contained" onClick={handleClick} disabled={isLoading}> | |
Create New Version | |
</Button> | |
{isLoading && <CircularProgress size={24} sx={{ ml: 1 }} />} | |
</Box> | |
); | |
}; | |
export default VersionCreator; | |
src/models/entry.model.ts | |
export interface Entry { | |
id: string; | |
createdAt: string; | |
prompt: string; | |
templateVariables: string; | |
versions: Version[]; | |
likes: number; | |
dislikes: number; | |
} | |
export interface Version { | |
id: string; | |
createdAt: string; | |
responseText: string; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment