Skip to content

Commit

Permalink
Merge pull request #1 from math-silva:feature/analytics-api
Browse files Browse the repository at this point in the history
feat: add analytics functionality
  • Loading branch information
math-silva committed May 16, 2024
2 parents 7ae5903 + 8067941 commit 7bd21b6
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 80 deletions.
31 changes: 31 additions & 0 deletions app/api/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
6 changes: 6 additions & 0 deletions app/api/analytics/total/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { dailyUsages } from '../../../lib/kv';

export async function GET() {
const totalUsage = await dailyUsages.hgetall('total_usage');
return Response.json({ totalUsage });
}
7 changes: 5 additions & 2 deletions app/api/gemini/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
}
}
84 changes: 57 additions & 27 deletions app/exercises/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<formDataProps | undefined>();

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}.
Expand All @@ -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 (
<main className="p-4 md:p-8 lg:py-12 lg:px-24">
<div className="flex flex-col items-center gap-8">
<Logo />
{
isGenerated ? (
<div className="py-6 px-8 w-auto bg-white rounded-lg border-1 border-gray-200 shadow-lg">
<Markdown className="prose lg:prose-lg" children={exercises} />
<div className="mt-2 pt-6 border-t-2 border-gray-300 flex flex-col items-start">
<button
onClick={() => setIsGenerated(false)}
className="text-blue-500 hover:text-blue-700 mb-2 hover:underline"
>
Gerar nova lista de exercícios
</button>
<Link href={"/"} className="text-blue-500 hover:text-blue-700 hover:underline">
Voltar ao início
</Link>

<>
<main className="p-4 md:p-8 lg:py-12 lg:px-24">
<div className="flex flex-col items-center gap-8">
<Logo />
{
isGenerated ? (
<div className="py-6 px-8 w-auto bg-white rounded-lg border-1 border-gray-200 shadow-lg">
<Markdown className="prose lg:prose-lg" children={exercises} />
<div className="mt-2 pt-6 border-t-2 border-gray-300 flex flex-col items-start">
<button
onClick={() => setIsGenerated(false)}
className="text-blue-500 hover:text-blue-700 mb-2 hover:underline"
>
Gerar nova lista de exercícios
</button>
<Link href={"/"} className="text-blue-500 hover:text-blue-700 hover:underline">
Voltar ao início
</Link>

</div>
</div>
</div>
) : (
<Form name={"lista de exercícios"} onSubmit={handleFormSubmit} />
)
}
</div>
</main>
) : (
<Form name={"lista de exercícios"} onSubmit={handleFormSubmit} />
)
}
</div>
</main>
{showCount && (
<p className="text-center text-white py-4">
O EstudAI já gerou <span className="text-yellow-400 underline">{exerciseCount}</span> listas de exercícios!
</p>
)}
</>
);
}

Expand Down
72 changes: 49 additions & 23 deletions app/flashcards/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<formDataProps | undefined>();

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}
Expand Down Expand Up @@ -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 (
<main className="p-8 lg:py-12 lg:px-24">
<div className="flex flex-col items-center gap-8">
<Logo />
{
isGenerated ? (
<div className="flex flex-col justify-center items-center">
<Flashcards text={flashcards} />
<button
className="mt-6 -mb-6 text-white hover:underline"
onClick={() => setIsGenerated(false)}
>
Gerar mais flashcards
</button>
</div>
) : (
<Form name={"flashcards"} onSubmit={handleFormSubmit} />
)
}
</div>
</main>
<>
<main className="p-8 lg:py-12 lg:px-24">
<div className="flex flex-col items-center gap-8">
<Logo />
{
isGenerated ? (
<div className="flex flex-col justify-center items-center">
<Flashcards text={flashcards} />
<button
className="mt-6 -mb-6 text-white hover:underline"
onClick={() => setIsGenerated(false)}
>
Gerar mais flashcards
</button>
</div>
) : (
<Form name={"flashcards"} onSubmit={handleFormSubmit} />
)
}
</div>
</main>
{showCount && (
<p className="text-center text-white py-4">
O EstudAI já gerou <span className="text-yellow-400 underline">{flashcardCount}</span> flashcards!
</p>
)}
</>
);
}

Expand Down
2 changes: 1 addition & 1 deletion app/lib/axios.ts
Original file line number Diff line number Diff line change
@@ -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',
}
Expand Down
37 changes: 37 additions & 0 deletions app/lib/kv.ts
Original file line number Diff line number Diff line change
@@ -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<DailyUsage> {
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;
}
Loading

0 comments on commit 7bd21b6

Please sign in to comment.