Archive Commit
This commit is contained in:
7
demo/.env.example
Normal file
7
demo/.env.example
Normal 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
3
demo/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.env
|
||||
5
demo/.vscode/settings.json
vendored
Normal file
5
demo/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.ignoreWords": [
|
||||
"openai"
|
||||
]
|
||||
}
|
||||
207
demo/app.js
Normal file
207
demo/app.js
Normal 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
24
demo/frontend/.gitignore
vendored
Normal 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
13
demo/frontend/index.html
Normal 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
3459
demo/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
demo/frontend/package.json
Normal file
24
demo/frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
demo/frontend/postcss.config.cjs
Normal file
6
demo/frontend/postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
40
demo/frontend/src/App.jsx
Normal file
40
demo/frontend/src/App.jsx
Normal 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
|
||||
40
demo/frontend/src/TransactionController.jsx
Normal file
40
demo/frontend/src/TransactionController.jsx
Normal 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 }
|
||||
31
demo/frontend/src/TransactionsList.jsx
Normal file
31
demo/frontend/src/TransactionsList.jsx
Normal 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 }
|
||||
3
demo/frontend/src/index.css
Normal file
3
demo/frontend/src/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
10
demo/frontend/src/main.jsx
Normal file
10
demo/frontend/src/main.jsx
Normal 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>,
|
||||
)
|
||||
12
demo/frontend/tailwind.config.cjs
Normal file
12
demo/frontend/tailwind.config.cjs
Normal 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: [],
|
||||
}
|
||||
16
demo/frontend/vite.config.js
Normal file
16
demo/frontend/vite.config.js
Normal 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
40
demo/openai.js
Normal 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
2765
demo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
demo/package.json
Normal file
24
demo/package.json
Normal 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
28
demo/sortTransactions.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user