Archive Commit

This commit is contained in:
Caleb Braaten 2024-06-13 13:22:17 -07:00
commit 6015fa6048
29 changed files with 7462 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

35
README.md Normal file
View File

@ -0,0 +1,35 @@
# Ai Quikstart
## Overview
> This project is not supported and is really just for reference... (there was some cool pagination of requests to handle context windows of LLMs)
This repo contains content related to the 'capstone' of an accelerated tutoring series for some CEOs that were interested in learning to code and exploring AI.
The instructions folder contain the Readmes I put together to help guide them along with building out a simple web app that they were interested in. It also has a mix of things that are done already and some things that aren't. This is because it was worked on in a live instruction setting for them to flex their creativity. As such, follow the instructions at your own risk.
The demo folder contains some somewhat working code. The OpenAI model that was initially used is no longer supported so I have moved the model declaration to an environment variable. Because my trial has expired and this isn't a production app, I haven't tested that newer models work in the same manner.
## Screenshots
![Homepage](homepage.png)
![Sorted Transactions](./sortedtransactions.png)
## Getting started
1. Git clone repo
2. cd into /demo
3. `npm install`
4. Rename .env.example to .env
5. Add OpenAI secrets and remove DEMO=true if you want dynamic functionality
6. `npm run dev` to kick of the demo
> Node Version 20 and above required
## Limitations
Lots... This was a demo project 😂
Some notable call outs though are below.
1. Categories are hardcoded
2. You're relying on the guessing of an LLM
3. Once transactions are sorted, you must refresh to go again

7
demo/.env.example Normal file
View File

@ -0,0 +1,7 @@
# Required if not in demo mode
OPENAI_ORG_ID=<<Your Org ID>>
OPENAI_API_KEY=<<Your API Token>>
OPENAI_MODEL="gpt-3.5-turbo"
# Include to skip asking OpenAI and return harcoded values
DEMO=true

3
demo/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
node_modules
.env

5
demo/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"cSpell.ignoreWords": [
"openai"
]
}

207
demo/app.js Normal file
View File

@ -0,0 +1,207 @@
import express from 'express';
import logger from 'morgan';
import multer from 'multer';
import neatCsv from 'neat-csv'
import sortTransactions from './sortTransactions.js'
import path from 'path';
import {fileURLToPath} from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const app = express()
const port = 3000
app.use(express.json())
app.use(logger('dev'))
const storage = multer.memoryStorage()
const upload = multer({ storage: storage })
app.get('/api/hello', (req, res) => {
res.send(
`<h1>Hello Big World</h1>`
)
})
app.post('/api/upload/', upload.single('file'), async (req, res) => {
console.log()
if(process.env.DEMO == "true"){
res.send(tempTransactions)
console.log("Hit")
return
}
if(!req.file) {
res.send('No file uploaded.')
return
}
const csvString = req.file.buffer.toString('utf8')
let results = await sortTransactions(csvString,
['Bills', 'Groceries', 'Restaurants', 'Entertainment', 'Shopping', 'Travel']
)
let transactions = await neatCsv(results)
// res.send(transactions)
})
app.use('/', express.static(path.join(__dirname, 'public')))
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
var tempTransactions = [
{
"Status": "Cleared",
"Date": "02/03/2023",
"Description": "CRUNCHYROLL",
"Debit": "11.03",
"Credit": "",
"Category": "Entertainment"
},
{
"Status": "Cleared",
"Date": "02/01/2023",
"Description": "GOOGLE CLOUD",
"Debit": "0.03",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/30/2023",
"Description": "FAMILY CH",
"Debit": "50.00",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/30/2023",
"Description": "PUD",
"Debit": "89.05",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET.COM",
"Debit": "5.00",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET.COM",
"Debit": "25.57",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "Amazon.com",
"Debit": "5.62",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET",
"Debit": "8.43",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/22/2023",
"Description": "GOOGLE YouTubePremium",
"Debit": "16.57",
"Credit": "",
"Category": "Entertainment"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "PEROT MUSEUM CAFE QPS DALLAS TX",
"Debit": "25.82",
"Credit": "",
"Category": "Restaurants"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "THE HOME DEPOT",
"Debit": "19.14",
"Credit": "",
"Category": "Groceries"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "Amazon.com",
"Debit": "13.12",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "AMZN Mktp",
"Debit": "6.55",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "STARBUCKS STORE",
"Debit": "10.28",
"Credit": "",
"Category": "Restaurants"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "TARGET.COM",
"Debit": "52.74",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "ZIPLY FIBER",
"Debit": "50.94",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/18/2023",
"Description": "7-ELEVEN 39782 DFW AIRPORT TX",
"Debit": "1.72",
"Credit": "",
"Category": "Travel"
},
{
"Status": "Cleared",
"Date": "01/18/2023",
"Description": "BUC-EE'S #36 TERRELL TX",
"Debit": "21.53",
"Credit": "",
"Category": "Travel"
},
{
"Status": "Cleared",
"Date": "01/17/2023",
"Description": "DALLAS ZOO MANAGEMENT DALLAS TX",
"Debit": "40.00",
"Credit": "",
"Category": "Entertainment"
}
]

24
demo/frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

13
demo/frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

3459
demo/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.21",
"tailwindcss": "^3.2.6",
"vite": "^4.1.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

40
demo/frontend/src/App.jsx Normal file
View File

@ -0,0 +1,40 @@
import {useState} from 'react';
import {TransactionController} from './TransactionController';
function App() {
const [file, setFile] = useState(null);
const [transactions, setTransactions] = useState([]);
function handleUpload() {
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload/', {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
setTransactions(data);
})
}
return (
<div className="bg-gray-100 h-screen flex flex-col justify-center items-center">
<div className="bg-white rounded-lg shadow-lg p-8">
<h1 className="text-2xl font-bold mb-4">Welcome to the Budget App</h1>
<p className="text-gray-500 mb-4">This is a simple app that will help you categorize your expenses.</p>
<p className="text-gray-500 mb-4">Upload a CSV file and we will do the rest.</p>
<div className="flex justify-center">
<input type="file" className="" onChange={(e) => setFile(e.target.files[0])} />
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={() => handleUpload()}>
Upload CSV
</button>
</div>
</div>
<TransactionController transactions={transactions} />
</div>
)
}
export default App

View File

@ -0,0 +1,40 @@
import React from 'react';
import {TransactionsList} from './TransactionsList';
function TransactionController({transactions}) {
if(transactions.length === 0) {
return (
<div className="bg-white rounded-lg shadow-lg p-8 mt-6">
<h1 className="text-2xl font-bold mb-4">Transactions</h1>
<p className="text-gray-500 mb-4">No transactions to display.</p>
</div>
)
}
for(let i = 0; i < transactions.length; i++) {
transactions[i].id = i;
}
// Sort transactions by category and render a transactions list for each category
let sortedTransactions = {};
transactions.map((transaction) => {
if(sortedTransactions[transaction.Category] === undefined) {
sortedTransactions[transaction.Category] = [];
sortedTransactions[transaction.Category].push(transaction);
} else {
sortedTransactions[transaction.Category].push(transaction);
}
})
return (
<div className="bg-white rounded-lg shadow-lg p-8 mt-6">
<h1 className="text-2xl font-bold mb-4">Transactions</h1>
{Object.keys(sortedTransactions).map((category) => (
<TransactionsList key={category} category={category} transactions={sortedTransactions[category]} />
))}
</div>
)
}
export { TransactionController }

View File

@ -0,0 +1,31 @@
import React from 'react';
function TransactionsList({category, transactions}) {
return (
<div className="bg-slate-100 rounded-lg shadow-lg p-8 mt-6">
<h1 className="text-2xl font-bold mb-4">{category}</h1>
<table className="table-auto">
<thead>
<tr>
<th className="px-4 py-2">Date</th>
<th className="px-4 py-2">Description</th>
<th className="px-4 py-2">Credit</th>
<th className="px-4 py-2">Debit</th>
</tr>
</thead>
<tbody>
{transactions.map((transaction) => (
<tr key={transaction.id}>
<td className="border px-4 py-2">{transaction.Date}</td>
<td className="border px-4 py-2">{transaction.Description}</td>
<td className="border px-4 py-2">{transaction.Credit}</td>
<td className="border px-4 py-2">{transaction.Debit}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export { TransactionsList }

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}

View File

@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server:{
proxy:{
"/api" : {
target: 'http://localhost:3000/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}
})

40
demo/openai.js Normal file
View File

@ -0,0 +1,40 @@
import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
organization: process.env.OPENAI_ORG_ID,
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);
export default async function (transactions, categories) {
if (!configuration.organization) {
return "OpenAI Organization Not Set";
}
if (!configuration.apiKey) {
return "OpenAI API key not configured";
}
try {
const completion = await openai.createCompletion({
model: process.env.OPENAI_MODEL,
prompt: generatePrompt(transactions, categories),
temperature: 0.0,
max_tokens: 2000,
});
return completion.data.choices[0].text;
} catch (error) {
// Consider adjusting the error handling logic for your use case
if (error.response) {
console.error(error.response.status, error.response.data);
} else {
console.error(`Error with OpenAI API request: ${error.message}`);
}
return "Failed to sort transactions"
}
}
function generatePrompt(transactions, categories) {
return `Add one of the following categories to each transaction: ${categories}
${transactions}`;
}

2765
demo/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
demo/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "concurrently \"nodemon --env-file=.env app.js\" \"cd frontend && npm run dev\""
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"neat-csv": "^7.0.0",
"openai": "^3.1.0"
},
"devDependencies": {
"concurrently": "^7.6.0",
"nodemon": "^2.0.20"
}
}

28
demo/sortTransactions.js Normal file
View File

@ -0,0 +1,28 @@
import returnCategoriesCSV from './openai.js';
export default async function (transactions, categories) {
// Split the transactions into array elements and pull out headers
transactions = transactions.split('\n');
const headers = transactions.shift();
// Group the transactions into strings containing chunks of 10 transactions each
const chunks = [];
for (let i = 0; i < transactions.length; i += 10) {
chunks.push(transactions.slice(i, i + 10).join('\n'));
}
let categorizedTransactions = []
for(let i = 0; i < chunks.length; i++) {
const returnedCSVText = await returnCategoriesCSV(headers+',Category\n'+chunks[i], categories);
const returnedTransactions = returnedCSVText.split('\n').slice(3); // Remove blank lines and headers return array
categorizedTransactions = categorizedTransactions.concat(returnedTransactions);
}
// Add headers back to the transactions
categorizedTransactions.unshift(headers+',Category');
const finalCSV = categorizedTransactions.join('\n').toString()
return finalCSV
}

BIN
homepage.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

72
instructions/README.md Normal file
View File

@ -0,0 +1,72 @@
# Project Scaffold
1. Initialize a node project to be able to install npm dependencies
```bash
npm init
```
2. Install the following backend dependencies that we will need
| App Dependencies | Dev Dependencies |
| - | - |
| express | nodemon |
| morgan | concurrently |
| multer | |
| csvtojson | |
| openai | |
3. Initialize a Vite React Project into a new subfolder called frontend
```bash
npm create vite@latest frontend --template react
```
4. `cd` into the frontend directory and install dependencies
```bash
npm install
```
5. In the frontend directory update vite.config.js to be as follows. This will allow us to proxy requests at `/api/`from our frontend to the backend.
```js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server:{
proxy:{
"/api" : {
target: 'http://localhost:3000/api/',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}
})
```
6. Add the following script to the root package.json file.
```json
"dev": "concurrently \"nodemon app.js\" \"cd frontend && npm run dev\""
```
7. Make an `app.js` in the root directory and make a boilerplate express app that will work with our frontend.
```js
const path = require('path')
const express = require('express')
const logger = require('morgan')
const app = express()
const port = 3000
app.use(express.json())
app.use(logger('dev'))
app.get('/api/hello', (req, res) => {
res.send(
`<h1>Hello World</h1>`
)
})
app.use('/', express.static(path.join(__dirname, 'public')))
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
```
8. You can now start the backend and frontend server with the same command `npm run dev` from the root of your project.
9. Open the url that Vite reports (should be something along the lines of `http://127.0.0.1:<port>`) and the default Vite app should render. Add `/api/hello` to the url and you should see the header from our express server route. This means we have successfully bootstrapped our project.

121
instructions/README2.md Normal file
View File

@ -0,0 +1,121 @@
# Building Out The Frontend
## Clearing the Vite Boilerplate
1. Delete the `src` folder and the `public` folder.
2. Create a new `src` folder and a new `public` folder.
3. Create a new `main.jsx` file in the `src` folder and add the following boilerplate code.
```jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
```
4. Create a new `App.jsx` file in the `src` folder and add the following boilerplate code.
```jsx
function App() {
return (
<div>Hello</div>
)
}
export default App
```
## Integrating TailwindCSS
1. Install TailwindCSS and PostCSS in your `frontend` directory.
```bash
npm install -D tailwindcss postcss autoprefixer
```
2. Create a new `postcss.config.cjs` file in the root of the project and add the following code.
```js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
```
3. Create a new `tailwind.config.cjs` file in the root of the project and add the following code.
```js
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
```
4. Create a new `index.css` file in the `src` folder and add the following code.
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
5. Update the `App.jsx` file to be as follows.
```jsx
function App() {
return (
<div className="bg-gray-100 h-screen flex justify-center items-center">
<div className="bg-white rounded-lg shadow-lg p-8">
<h1 className="text-2xl font-bold mb-4">Welcome to the Budget App</h1>
<p className="text-gray-500 mb-4">This is a simple app that will help you categorize your expenses.</p>
<p className="text-gray-500 mb-4">Upload a CSV file and we will do the rest.</p>
<div className="flex justify-center">
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Upload CSV
</button>
</div>
</div>
</div>
)
}
```
6. You should now see a basic app with a button that says "Upload CSV". We can now start building out the functionality of the app and any styling can continu to be done with TailwindCSS.
## Adding File Upload Functionality
1. Import useState into `App.jsx` and add a state variable called `file` that will hold the file that is uploaded.
```jsx
import { useState } from 'react'
```
2. Add the following code to the `App` function.
```jsx
const [file, setFile] = useState(null)
```
3. Add a file selector input just above the `UploadCSV` button.
```jsx
<input type="file" onChange={(e) => setFile(e.target.files[0])} />
```
4. Add an onClick attribute to the `UploadCSV` button and add a callback function to handle the upload.
5. Next, define the callback function that will handle the upload in between your useState and return statements. For now, we will just console log the file name.
```jsx
console.log(file.name)
```
4. You should now be able to select a file and see the file name in the console. We can now start building out the backend to handle the file upload to the server.
5. Next we will make a form to handle the request to the backend server. Replace the console.log statement with the following code.
```jsx
const formData = new FormData()
formData.append('file', file)
```
6. Next, we will make a POST request to the backend server. Make the following fetch request.
```jsx
fetch('/api/upload', {
method: 'POST',
body: formData,
})
```
7. fetch is a network request that returns a promise. We can use the promise to handle the response from the server. Add the following code to handle the response.
```jsx
.then((res) => res.json())
.then((data) => console.log(data))
```
8. You should now be able to upload a file and see the response in the console. We can now start building out the backend to handle the file upload to the server.

84
instructions/README3.md Normal file
View File

@ -0,0 +1,84 @@
# Building Out The Backend
## Handling the File Upload
1. In the `app.js` file, add multer as a dependency and configure it to store files in a memory buffer.
```js
const multer = require('multer')
const storage = multer.memoryStorage()
const upload = multer({ storage: storage })
```
2. Add a new route to the app that will handle the file upload. (This should match the path from the frontend)
```js
app.post('/api/upload', (req, res) => {
console.log(req.file)
res.send("File Uploaded")
})
```
3. The above route will log the file object to the terminal. We can use this to make sure the file is being uploaded correctly. We then close the connection with the client by sending a response of "File Uploaded". The challenge is however that `req.file` is undefined. This is because we haven't added the `upload.single('file')` middleware to the route. We will do this in the next step.
4. Add the `upload.single('file')` middleware to the route. This will parse the file from the request body and store it in `req.file` for us anytime a file is included in a request to that route. This is why middleware is so great, it allows for simplistic extensibility.
```js
app.post('/api/upload', upload.single('file'), (req, res) => { ...})
```
4. You can now test the route by selecting a file in the frontend and clicking the "Upload CSV" button. You should see the file object logged in the terminal of your server.
## Making Sense of the CSV File
1. When we upload a file, we get a file object back. This object contains a lot of information about the file, including the file name, the file type, and the file size. It also contains a buffer that contains the actual contents of the file. We can use the buffer to read the contents of the file.
2. Read the buffer and convert it to a string. We can do this by using the `toString()` method on the buffer. The 'utf8' argument tells the method to convert the buffer to a string using the utf8 encoding. (as opposed to ascii, base64, etc.)
```js
const csvString = req.file.buffer.toString('utf8')
```
3. We can now log csvString to the terminal to see the contents of the file. You should see a string that contains the contents of the file (all our transactions represented as a string).
## Adding a Little Error Handling
1. You may end up accidentally not selecting a file when you click the "Upload CSV" button. This will cause the server to crash because `req.file` will be undefined. We can add a little error handling to prevent this from happening.
2. Add an if statement to the route that checks if `req.file` is undefined. If it is, send a message to the client.
```js
if (!req.file) {
res.status(400).send("No file uploaded")
}
```
## Setting up Our Sorting Magic
1. Because we will have a lot of logic for sorting the transactions, we will create a new file to handle this. Create a new file called `sortTransactions.js` in the root directory next to app.js
2. In the `sortTransactions.js` file, export a default function that takes a string and an array as an argument. This string will be the contents of the csv file and the array will be the list of categories.
```js
export default function (transactions, categories) {
// logic to sort transactions
}
```
3. For now just return a string that says "Hello World". We will add the logic later.
4. Import the `sortTransactions` function into the `app.js` file.
```js
import sortTransactions from './sortTransactions.js'
```
6. You may notice this import looks a little different than the ones we have been using. This is because we are using ESM imports. This is the new standard for importing modules in JavaScript. You can read more about it [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import). It is also what we have been using in the frontend but it's a recent addition to Node which used to only support CommonJS imports. You can read more about that [here](https://nodejs.org/api/modules.html#modules_modules_commonjs_modules).
7. Refactor your imports to use ESM imports.
```js
import express from 'express'
import multer from 'multer'
import sortTransactions from './sortTransactions.js'
```
8. Call the `sortTransactions` function in the route and pass in the csvString and the categories array. Go ahead and hardcode the categories array for now.
```js
const sortedTransactions = sortTransactions(csvString, ['Bills', 'Groceries', 'Restaurants', 'Entertainment', 'Shopping', 'Travel'])
```
9. Return the sortedTransactions to the client.
```js
res.send(sortedTransactions)
```
// Not shared below this line
Notes:
- Migrate to app.js to use ESM Imports
- Add "type": "module" to package.json
- Use neat-csv to parse csv to json onject
- Need to make a [__dirname](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#what-do-i-use-instead-of-__dirname-and-__filename) variable to get the path to the static folder
Add to frontend/src/App.jsx to see the return response.
```js
.then((response) => response.json())
.then((data) => {
console.log(data);
})
```

91
instructions/README4.md Normal file
View File

@ -0,0 +1,91 @@
# Challenge Starter
This week we will be integrating with OpenAI. Some code will be given to you because the focus of today will be around researching how to authenticate to OpenAI's API and how the API responds based on a given input. We want the response to be as deterministic as possible and you will need to write a prompt that can satify this requirement.
## OpenAI
1. Make a new file called openai.js in the root directory with the following contents:
```js
import { Configuration, OpenAIApi } from "openai";
const configuration = new Configuration({
organization: "<<Org ID>>",
apiKey: "<<API Key>>",
});
const openai = new OpenAIApi(configuration);
export default async function (transactions, categories) {
if (!configuration.apiKey) {
return "OpenAI API key not configured";
}
try {
const completion = await openai.createCompletion({
model: "text-davinci-003",
prompt: generatePrompt(transactions, categories),
temperature: 0.0,
max_tokens: 2000,
});
return completion.data.choices[0].text;
} catch (error) {
// Consider adjusting the error handling logic for your use case
if (error.response) {
console.error(error.response.status, error.response.data);
} else {
console.error(`Error with OpenAI API request: ${error.message}`);
}
return "Failed to sort transactions"
}
}
function generatePrompt(transactions, categories) {
return ``;
}
```
2. Spend some time understanding how the code works and refactor it to meet your purposes. Again, you will need a prompt that is deterministic and can be used to sort transactions. Because the API can be tough to test, OpenAI has given us a [playground](https://platform.openai.com/playground) to experiment within.
3. Inside your sortTransactions.js file, import the openai.js file and call the function with the transactions and categories. You will need to await the response and then return it.
## Route Endpoint
1. In your app.js file, we will import `neat-csv` to parse the csv text we receive from `sortTransactions` to transform it into a json object and return that to the client. Your complete endpoint should look something like this:
```js
app.post('/api/upload/', upload.single('file'), async (req, res) => {
if(!req.file) {
res.send('No file uploaded.')
return
}
const csvString = req.file.buffer.toString('utf8')
let results = await sortTransactions(csvString,
['Bills', 'Groceries', 'Restaurants', 'Entertainment', 'Shopping', 'Travel']
)
let transactions = await neatCsv(results)
res.send(transactions)
})
```
## sortTransactions.js Starting Point
```js
import returnCategoriesCSV from './openai.js';
export default async function (transactions, categories) {
returnedCSVText = await returnCategoriesCSV(transactions, categories);
return returnedCSVText
}
```
## Getting it working through the entire stack
You may be asking yourself what is the point of `sortTransactions.js`. You are handling the request and response in `app.js` and you are handling the sorting in `openai.js`. But in between you have the `sortTransactions.js` file. This is where you will be doing the work of parsing the inputs and outputs from the client and openAI in a structured manner taking into account the considerations below. Pseudo code and remembering your advanced array methods like .split() and .join() will be *incredibly* helpful here.
There is no "right solution" to this challenge. The goal is to get you thinking about how to structure your code and how to think about the problem. I've solved it in one manner but there are many ways to do it. Be prepared to spend a lot of your effort on thinking about the conceptual steps you will need to take.
### Considerations
- What is async/await and how does it work?
- How many transactions can you sort at once?
- How many requests to the API can you make every minute?
### Warning
- The API will be slow, it has to do a lot of work as a Large Language Model.

301
instructions/README5.md Normal file
View File

@ -0,0 +1,301 @@
# Frontend Rendering
The goal is to now render the transactions in the browser. There are many ways to render this to the user, the first is to list out all the transactions at once the other is to segment them by category. The first is easier to implement but the second is more useful.
Below are some code snippets to help you get started. You'll have to compose them yourself and add additional logic.
## A Generic Component
```jsx
import React from 'react';
function ComponentName({pro1, prop2}) {
return (
<h1>ComponentName</h1>
)
}
export { ComponentName }
```
## Using a Generic Component
```jsx
import React from 'react';
import {TransactionsList} from './TransactionsList';
function HigherOrderComponent() {
return (
<div>
<TransactionsList prop1={data} />
<TransactionsList prop1={data} />
</div>
);
}
```
## Rendering a Card
```jsx
<div className="bg-white rounded-lg shadow-lg p-8 mt-6">
<h1 className="text-2xl font-bold mb-4">Transactions</h1>
<p className="text-gray-500 mb-4">No transactions to display.</p>
</div>
```
## Rendering a Table
```jsx
<div className="bg-slate-100 rounded-lg shadow-lg p-8 mt-6">
<h1 className="text-2xl font-bold mb-4">Title</h1>
<table className="table-auto">
<thead>
<tr>
<th className="px-4 py-2">Heading 1</th>
<th className="px-4 py-2">Heading 2</th>
</tr>
</thead>
<tbody>
{transactions.map((array) => (
<tr key={arrayItem.id}>
<td className="border px-4 py-2">{datavalue1}</td>
<td className="border px-4 py-2">{datavalue2}</td>
</tr>
))}
</tbody>
</table>
</div>
```
# Helpful Tips
## Capturing State from the Backend
```jsx
const [transactions, setTransactions] = useState([]);
function handleUpload() {
const formData = new FormData();
formData.append('file', file);
fetch('/api/upload/', {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
setTransactions(data);
})
}
```
## Faster Backend Testing
Instead of using OpenAI to process transactions for testing, just return the following json in app.js. This will allow you to test your frontend rendering without having to wait for the OpenAI API to respond and not incur extra charges for API use.
```js
var tempTransactions = [
{
"Status": "Cleared",
"Date": "02/03/2023",
"Description": "CRUNCHYROLL *MEMBERSHI 415-503-9235 CA",
"Debit": "11.03",
"Credit": "",
"Category": "Entertainment"
},
{
"Status": "Cleared",
"Date": "02/02/2023",
"Description": "CITY OF MARYSVILLE, WA MARYSVILLE WA",
"Debit": "240.50",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "02/01/2023",
"Description": "GOOGLE CLOUD XW9DZ4 650-253-0000 CA",
"Debit": "0.03",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/30/2023",
"Description": "STATE STREET FAMILY CH MARYSVILLE WA",
"Debit": "50.00",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/30/2023",
"Description": "SNOHOMISH COUNTY PUD 425-783-1000 WA",
"Debit": "89.05",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/28/2023",
"Description": "HAGGEN 3604 MARYSVILLE WA",
"Debit": "9.77",
"Credit": "",
"Category": "Groceries"
},
{
"Status": "Cleared",
"Date": "01/28/2023",
"Description": "SP HIYA HEALTH BOCA RATON FL",
"Debit": "32.82",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET.COM * 800-591-3869 MN",
"Debit": "5.00",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET.COM * 800-591-3869 MN",
"Debit": "25.57",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "PINEWOOD FAMILY DENTAL MARYSVILLE WA",
"Debit": "485.00",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "Amazon.com2I8ZG7YJ3 Amzn.com/bill WA null XXXXXXXXXXXX9998",
"Debit": "5.62",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "TARGET 00021923 MARYSVILLE WA null XXXXXXXXXXXX8138",
"Debit": "8.43",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/24/2023",
"Description": "ONLINE PAYMENT, THANK YOU",
"Debit": "",
"Credit": "-4182.23",
"Category": "",
"_6": ""
},
{
"Status": "Cleared",
"Date": "01/23/2023",
"Description": "STATE STREET FAMILY CH MARYSVILLE WA",
"Debit": "50.00",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/22/2023",
"Description": "GOOGLE YouTubePremium 650-253-0000 CA",
"Debit": "16.57",
"Credit": "",
"Category": "Entertainment"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "PEROT MUSEUM CAFE QPS DALLAS TX",
"Debit": "25.82",
"Credit": "",
"Category": "Restaurants"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "THE HOME DEPOT #4726 MARYSVILLE WA",
"Debit": "19.14",
"Credit": "",
"Category": "Groceries"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "Amazon.com4L04G50H3 Amzn.com/bill WA null XXXXXXXXXXXX9998",
"Debit": "13.12",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/20/2023",
"Description": "AMZN Mktp US*622L107Y3 Amzn.com/bill WA null XXXXXXXXXXXX9998",
"Debit": "6.55",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "STARBUCKS STORE 03321 MARYSVILLE WA",
"Debit": "10.28",
"Credit": "",
"Category": "Restaurants"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "STATE STREET FAMILY CH MARYSVILLE WA",
"Debit": "50.00",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "TARGET.COM * 800-591-3869 MN",
"Debit": "52.74",
"Credit": "",
"Category": "Shopping"
},
{
"Status": "Cleared",
"Date": "01/19/2023",
"Description": "ZIPLY FIBER * INTERNET 866-699-4759 WA",
"Debit": "50.94",
"Credit": "",
"Category": "Bills"
},
{
"Status": "Cleared",
"Date": "01/18/2023",
"Description": "7-ELEVEN 39782 DFW AIRPORT TX",
"Debit": "1.72",
"Credit": "",
"Category": "Travel"
},
{
"Status": "Cleared",
"Date": "01/18/2023",
"Description": "BUC-EE'S #36 TERRELL TX",
"Debit": "21.53",
"Credit": "",
"Category": "Travel"
},
{
"Status": "Cleared",
"Date": "01/17/2023",
"Description": "DALLAS ZOO MANAGEMENT DALLAS TX",
"Debit": "40.00",
"Credit": "",
"Category": "Entertainment"
}
]
```

BIN
sortedtransactions.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB