From f9f9086410e4f891b19b1c5cc2d989c070c28362 Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:57:41 -0300 Subject: [PATCH 1/6] chore: update npm dependencies --- package-lock.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 47 insertions(+) diff --git a/package-lock.json b/package-lock.json index a1e57d1..2e04f8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.1.0", "dependencies": { "@google/generative-ai": "^0.11.1", + "@vercel/kv": "^1.0.1", "axios": "^1.6.8", + "moment-timezone": "^0.5.45", "next": "14.2.3", "react": "^18", "react-dom": "^18", @@ -421,6 +423,25 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, + "node_modules/@upstash/redis": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.25.1.tgz", + "integrity": "sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==", + "dependencies": { + "crypto-js": "^4.2.0" + } + }, + "node_modules/@vercel/kv": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-1.0.1.tgz", + "integrity": "sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==", + "dependencies": { + "@upstash/redis": "1.25.1" + }, + "engines": { + "node": ">=14.6" + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -719,6 +740,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1898,6 +1924,25 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index b5912d2..092251b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ }, "dependencies": { "@google/generative-ai": "^0.11.1", + "@vercel/kv": "^1.0.1", "axios": "^1.6.8", + "moment-timezone": "^0.5.45", "next": "14.2.3", "react": "^18", "react-dom": "^18", From a0d02af9fe71b134964bc07db2ef651215235441 Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:57:55 -0300 Subject: [PATCH 2/6] refactor(api): update axios base URL for API requests --- app/lib/axios.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/axios.ts b/app/lib/axios.ts index 25b0d48..7151302 100644 --- a/app/lib/axios.ts +++ b/app/lib/axios.ts @@ -1,7 +1,7 @@ import axios from "axios"; export const api = axios.create({ - baseURL: process.env.NEXT_PUBLIC_BASE_URL || "https://projetoestudai.vercel.app/", + baseURL: process.env.NEXT_PUBLIC_BASE_URL || "https://projetoestudai.vercel.app/api/", headers: { 'Content-Type': 'application/json', } From 0f409315c8501147598a0f082ddf9a5fc175d496 Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:58:05 -0300 Subject: [PATCH 3/6] refactor: add KV client and functions for daily usage tracking --- app/lib/kv.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 app/lib/kv.ts diff --git a/app/lib/kv.ts b/app/lib/kv.ts new file mode 100644 index 0000000..1ee82b7 --- /dev/null +++ b/app/lib/kv.ts @@ -0,0 +1,37 @@ +import { createClient } from "@vercel/kv"; + +export interface DailyUsage { + flashcardCount: number; + summaryCount: number; + exerciseCount: number; + usageCount: number; + inputTokenCount: number; + outputTokenCount: number; + totalTokenCount: number; +} + +export const dailyUsages = createClient({ + url: process.env.KV_REST_API_URL || "", + token: process.env.KV_REST_API_TOKEN || "", +}); + +export async function getOrInitializeDailyUsage(today: string): Promise { + const usage = await dailyUsages.hgetall(`daily_usage:${today}`) as unknown as DailyUsage; + + if (!usage) { + const defaultUsage: DailyUsage = { + flashcardCount: 0, + summaryCount: 0, + exerciseCount: 0, + usageCount: 0, + inputTokenCount: 0, + outputTokenCount: 0, + totalTokenCount: 0, + }; + + await dailyUsages.hset(`daily_usage:${today}`, { ...defaultUsage }); + return defaultUsage; + } + + return usage; +} \ No newline at end of file From 3b21867ed1e17ae52dec57189a68d1002a0893de Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:58:26 -0300 Subject: [PATCH 4/6] refactor: update gemini model and response handling --- app/api/gemini/route.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/gemini/route.ts b/app/api/gemini/route.ts index 3f0457b..11bd24e 100644 --- a/app/api/gemini/route.ts +++ b/app/api/gemini/route.ts @@ -11,8 +11,11 @@ export async function POST(req: Request) { const result = await model.generateContent(prompt); const response = await result.response; - const text = response.text() + const text = response.text(); + const inputTokenCount = response.usageMetadata?.promptTokenCount ?? 0; + const outputTokenCount = response.usageMetadata?.candidatesTokenCount ?? 0; + const totalTokenCount = inputTokenCount + outputTokenCount; - return Response.json({ generatedContent: text }); + return Response.json({ generatedContent: text, tokenInfo: { inputTokenCount, outputTokenCount, totalTokenCount } }); } } From df561b0c6353a935d9f9f8670267996f443218b8 Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:58:39 -0300 Subject: [PATCH 5/6] feat: Add API routes for analytics and total usage tracking --- app/api/analytics/route.ts | 31 +++++++++++++++++++++++++++++++ app/api/analytics/total/route.ts | 6 ++++++ 2 files changed, 37 insertions(+) create mode 100644 app/api/analytics/route.ts create mode 100644 app/api/analytics/total/route.ts diff --git a/app/api/analytics/route.ts b/app/api/analytics/route.ts new file mode 100644 index 0000000..74e7475 --- /dev/null +++ b/app/api/analytics/route.ts @@ -0,0 +1,31 @@ +import { dailyUsages, getOrInitializeDailyUsage } from '../../lib/kv'; +import moment from 'moment-timezone'; + +moment.tz.setDefault('America/Sao_Paulo'); + +export async function GET() { + const today = moment().format('YYYY-MM-DD'); + const usage = await getOrInitializeDailyUsage(today); + return Response.json({ usage }); +} + +export async function POST(req: Request) { + const { type, count, tokenInfo } = await req.json(); + const today = moment().format('YYYY-MM-DD'); + + try { + let usage = await getOrInitializeDailyUsage(today); + + await dailyUsages.hincrby(`daily_usage:${today}`, type, count); + await dailyUsages.hincrby(`daily_usage:${today}`, 'usageCount', 1); + await dailyUsages.hincrby(`daily_usage:${today}`, 'inputTokenCount', tokenInfo.inputTokenCount); + await dailyUsages.hincrby(`daily_usage:${today}`, 'outputTokenCount', tokenInfo.outputTokenCount); + await dailyUsages.hincrby(`daily_usage:${today}`, 'totalTokenCount', tokenInfo.totalTokenCount); + await dailyUsages.hincrby('total_usage', type, count); + await dailyUsages.hincrby('total_usage', 'usageCount', 1); + usage = await getOrInitializeDailyUsage(today); + return Response.json({ message: 'Usage updated', usage }); + } catch (error) { + return Response.json({ error: 'Failed to update usage' }, { status: 500 }); + } +} diff --git a/app/api/analytics/total/route.ts b/app/api/analytics/total/route.ts new file mode 100644 index 0000000..aee181f --- /dev/null +++ b/app/api/analytics/total/route.ts @@ -0,0 +1,6 @@ +import { dailyUsages } from '../../../lib/kv'; + +export async function GET() { + const totalUsage = await dailyUsages.hgetall('total_usage'); + return Response.json({ totalUsage }); +} \ No newline at end of file From 8067941ef4f7be781ff358cb2f429e76c586f383 Mon Sep 17 00:00:00 2001 From: Matheus Silva Date: Wed, 15 May 2024 22:59:33 -0300 Subject: [PATCH 6/6] feat: add analytics --- app/exercises/page.tsx | 84 ++++++++++++++++++++++++++++------------- app/flashcards/page.tsx | 72 ++++++++++++++++++++++++----------- app/summary/page.tsx | 79 +++++++++++++++++++++++++------------- 3 files changed, 158 insertions(+), 77 deletions(-) diff --git a/app/exercises/page.tsx b/app/exercises/page.tsx index 836253a..2c7cf4a 100644 --- a/app/exercises/page.tsx +++ b/app/exercises/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Markdown from "react-markdown"; import Link from "next/link"; import { api } from "../lib/axios"; @@ -15,10 +15,22 @@ interface formDataProps { } const ExercisesPage = () => { + const [exerciseCount, setExerciseCount] = useState(0); + const [showCount, setShowCount] = useState(false); const [exercises, setExercises] = useState(''); const [isGenerated, setIsGenerated] = useState(false); const [data, setData] = useState(); + useEffect(() => { + api.get('/analytics/total').then(response => { + const count = parseInt(response.data.totalUsage.exerciseCount) || 0; + setExerciseCount(count); + setShowCount(true); + }).catch((error: Error) => { + console.error(error); + }); + }, []); + const handleFormSubmit = async ({ educationLevel, subject, content }: formDataProps) => { const prompt = ` Crie uma lista de exercícios de ${subject} para um estudante de ${educationLevel}. @@ -33,42 +45,60 @@ const ExercisesPage = () => { A lista deve ser numerada. `; + setData({ + educationLevel, + subject, + content, + }); - api.post('/api/gemini/', { prompt: prompt }).then(response => { + api.post('/gemini/', { prompt: prompt }).then(response => { setExercises(response.data.generatedContent); setIsGenerated(true); + api.post('/analytics/', { + type: 'exerciseCount', + count: 1, + tokenInfo: response.data.tokenInfo + }); + setExerciseCount(prevExerciseCount => prevExerciseCount + 1); }).catch((error: Error) => { console.error(error); }); }; return ( -
-
- - { - isGenerated ? ( -
- -
- - - Voltar ao início - - + <> +
+
+ + { + isGenerated ? ( +
+ +
+ + + Voltar ao início + + +
-
- ) : ( -
- ) - } -
-
+ ) : ( + + ) + } + + + {showCount && ( +

+ O EstudAI já gerou {exerciseCount} listas de exercícios! +

+ )} + ); } diff --git a/app/flashcards/page.tsx b/app/flashcards/page.tsx index 2b403eb..d8f4f0a 100644 --- a/app/flashcards/page.tsx +++ b/app/flashcards/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { api } from "../lib/axios"; import Form from "../components/forms/Form"; @@ -14,13 +14,25 @@ interface formDataProps { } const FlashcardsPage = () => { + const [flashcardCount, setFlashcardCount] = useState(0); + const [showCount, setShowCount] = useState(false); const [flashcards, setFlashcards] = useState(''); const [isGenerated, setIsGenerated] = useState(false); const [data, setData] = useState(); + useEffect(() => { + api.get('/analytics/total').then(response => { + const count = parseInt(response.data.totalUsage.flashcardCount) || 0; + setFlashcardCount(count); + setShowCount(true); + }).catch((error: Error) => { + console.error(error); + }); + }, []); + const handleFormSubmit = ({ educationLevel, subject, content }: formDataProps) => { const prompt = ` - Crie de 9 a 12 flashcards numerados para a disciplina de ${subject} para um estudante do ${educationLevel}, abrangendo os seguintes tópicos: + Crie 12 flashcards numerados para a disciplina de ${subject} para um estudante do ${educationLevel}, abrangendo os seguintes tópicos: ${content} @@ -52,35 +64,49 @@ const FlashcardsPage = () => { content, }) - api.post('/api/gemini/', { prompt: prompt }).then(response => { + api.post('/gemini/', { prompt: prompt }).then(response => { setFlashcards(response.data.generatedContent); + const createdFlashcards = response.data.generatedContent.split('\n').length; setIsGenerated(true); + api.post('/analytics/', { + type: 'flashcardCount', + count: createdFlashcards, + tokenInfo: response.data.tokenInfo + }); + setFlashcardCount(prevFlashcardCount => prevFlashcardCount + createdFlashcards); }).catch((error: Error) => { console.error(error); }); }; return ( -
-
- - { - isGenerated ? ( -
- - -
- ) : ( - - ) - } -
-
+ <> +
+
+ + { + isGenerated ? ( +
+ + +
+ ) : ( + + ) + } +
+
+ {showCount && ( +

+ O EstudAI já gerou {flashcardCount} flashcards! +

+ )} + ); } diff --git a/app/summary/page.tsx b/app/summary/page.tsx index e049336..6a37fb9 100644 --- a/app/summary/page.tsx +++ b/app/summary/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import Markdown from "react-markdown"; import Link from "next/link"; import { api } from "../lib/axios"; @@ -15,10 +15,22 @@ interface formDataProps { } const SummaryPage = () => { + const [summaryCount, setSummaryCount] = useState(0); + const [showCount, setShowCount] = useState(false); const [summary, setSummary] = useState(''); const [isGenerated, setIsGenerated] = useState(false); const [data, setData] = useState(); + useEffect(() => { + api.get('/analytics/total').then(response => { + const count = parseInt(response.data.totalUsage.summaryCount) || 0; + setSummaryCount(count); + setShowCount(true); + }).catch((error: Error) => { + console.error(error); + }); + }, []); + const handleFormSubmit = ({ educationLevel, subject, content }: formDataProps) => { const prompt = ` Gere um resumo conciso e informativo sobre ${subject} para um estudante de ${educationLevel}. @@ -37,41 +49,54 @@ const SummaryPage = () => { content, }); - api.post('/api/gemini/', { prompt: prompt }).then(response => { + api.post('/gemini/', { prompt: prompt }).then(response => { setSummary(response.data.generatedContent); setIsGenerated(true); + api.post('/analytics/', { + type: 'summaryCount', + count: 1, + tokenInfo: response.data.tokenInfo + }); + setSummaryCount(prevSummaryCount => prevSummaryCount + 1); }).catch((error: Error) => { console.error(error); }); }; return ( -
-
- - { - isGenerated ? ( -
- -
- - - Voltar ao início - - + <> +
+
+ + { + isGenerated ? ( +
+ +
+ + + Voltar ao início + + +
-
- ) : ( - - ) - } -
-
+ ) : ( + + ) + } + + + {showCount && ( +

+ O EstudAI já gerou {summaryCount} resumos! +

+ )} + ); }