Skip to content

Instantly share code, notes, and snippets.

@brunoksato
Created October 6, 2025 17:06
Show Gist options
  • Save brunoksato/94f43a17f8186edc39c0a477e64d2eeb to your computer and use it in GitHub Desktop.
Save brunoksato/94f43a17f8186edc39c0a477e64d2eeb to your computer and use it in GitHub Desktop.
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { db } from './db';
import { cards } from './db/schema';
import { eq, lte, asc } from 'drizzle-orm';
const app = new Hono();
const PORT = 3000;
// --- Middlewares ---
app.use('/*', cors());
// --- ROTAS DA API ---
// GET /api/cards/due - Obter o próximo card devido
app.get('/api/cards/due', async (c) => {
try {
const today = new Date().toISOString().split('T')[0];
const [card] = await db
.select()
.from(cards)
.where(lte(cards.dueDate, today as string))
.orderBy(asc(cards.dueDate))
.limit(1);
return c.json(card || null);
} catch (error: any) {
return c.json({ error: error.message }, 500);
}
});
// POST /api/cards - Criar um novo card
app.post('/api/cards', async (c) => {
try {
const body = await c.req.json();
const front: string = body.front;
const back: string = body.back;
if (!front || !back) {
return c.json({ error: 'Front e back são obrigatórios' }, 400);
}
const today: string = new Date().toISOString().split('T')[0] as string;
const newCard = {
front,
back,
dueDate: today,
};
const result = await db
.insert(cards)
.values(newCard)
.returning();
if (!result?.[0]) {
throw new Error('Falha ao criar card');
}
return c.json({ id: result[0].id }, 201);
} catch (error: any) {
return c.json({ error: error.message }, 400);
}
});
// POST /api/cards/review/:id - Revisar um card (algoritmo SM-2)
app.post('/api/cards/review/:id', async (c) => {
try {
const id = parseInt(c.req.param('id'));
const { quality } = await c.req.json();
const [card] = await db
.select()
.from(cards)
.where(eq(cards.id, id))
.limit(1);
if (!card) {
return c.json({ error: 'Card não encontrado.' }, 404);
}
let { repetition, easinessFactor, interval } = card;
// Algoritmo SM-2
if (quality < 3) {
repetition = 0;
interval = 1;
} else {
repetition += 1;
if (repetition === 1) interval = 1;
else if (repetition === 2) interval = 6;
else interval = Math.round(interval * easinessFactor);
easinessFactor += (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
if (easinessFactor < 1.3) easinessFactor = 1.3;
}
const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + interval);
const nextDueDate = dueDate.toISOString().split('T')[0];
await db
.update(cards)
.set({
repetition,
easinessFactor,
interval,
dueDate: nextDueDate,
})
.where(eq(cards.id, id));
return c.json({ message: 'Card atualizado com sucesso!' });
} catch (error: any) {
return c.json({ error: error.message }, 500);
}
});
// --- ROTA DE GERAÇÃO DE CARDS COM HUGGING FACE ---
// Usando o modelo GPT-OSS-120b da OpenAI via Hugging Face Router (Apache 2.0 license)
// Referência: https://huggingface.co/openai/gpt-oss-120b
const HF_ROUTER_URL = "https://router.huggingface.co/v1/chat/completions";
const HF_MODEL = "openai/gpt-oss-120b:fireworks-ai";
app.post('/api/generate-cards', async (c) => {
try {
// generate the cound with base in the length of the text
const { text, textAnswer, topic } = await c.req.json();
const count = Math.ceil(text.length / 300);
if (!text && !topic) {
return c.json({ error: 'É necessário fornecer um texto ou um tópico.' }, 400);
}
// System messages for GPT-OSS (OpenAI Chat Completions format)
const systemPrompt = `You are an expert at creating effective study flashcards.
Your response must be ONLY a valid JSON array of objects, with no additional text, explanations, or markdown formatting.
Required format: [{"front": "Question?", "back": "Answer."}]
Do not include \`\`\`json or any other text—only the pure JSON array.
LANGUAGE REQUIREMENT:
- ALL flashcard content (both front and back) MUST be written in Brazilian Portuguese (pt-BR)
CRITICAL REQUIREMENTS:
- Each flashcard must contain ONE clear, focused concept
- Questions (front) must be specific and unambiguous
- Answers (back) must be accurate, concise, and complete
- Only include verifiable, factual information
- Avoid vague or overly general statements
- Use precise terminology appropriate to the subject matter
FORBIDDEN - DO NOT create flashcards that:
- Ask about the text structure or organization (e.g., "Why will X be studied later?", "What will be covered next?")
- Reference the text itself (e.g., "According to the text...", "The document says...")
- Ask meta-questions about pedagogy (e.g., "Why does this appear in the material?", "Why is this important to study?")
- Ask about test/exam appearances (e.g., "Why is this tested?", "Because it appears on tests")
- Focus ONLY on actual concepts, definitions, processes, facts, and relationships`;
const userContent = text
? `Based on the following text, generate exactly ${count} concise and effective flashcards for studying.
IMPORTANT RULES:
- Generate ALL flashcards in Brazilian Portuguese (pt-BR)
- Use ONLY information directly stated or clearly implied in the provided text
- Do not add external knowledge or assumptions
- Extract the most important concepts, definitions, and relationships
- Ask about WHAT, HOW, and WHEN concepts work, not about the text structure or organization
- Do NOT ask questions like "Why will this be studied later?" or "According to the text, what..."
Text:
"""${text}"""`
: `The user is struggling with the topic: "${topic}".
Create exactly ${count} reinforcement flashcards that explore different aspects and important concepts of this topic.
IMPORTANT:
- Generate ALL flashcards in Brazilian Portuguese (pt-BR)
Focus on:
- Fundamental concepts and key definitions
- How processes and mechanisms work
- Essential relationships and connections
- Practical applications and examples
- Ensure each flashcard addresses a distinct aspect for comprehensive coverage`;
// Formato padrão OpenAI Chat Completions
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: userContent }
];
const response = await fetch(HF_ROUTER_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer 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`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: HF_MODEL,
messages: messages,
max_tokens: 2048,
temperature: 0.7,
top_p: 0.95,
})
});
if (!response.ok) {
if (response.status === 401) {
return c.json({
error: 'Token da API do Hugging Face inválido ou expirado.'
}, 401);
}
if (response.status === 503) {
return c.json({
error: 'O modelo GPT-OSS está carregando. Modelos grandes podem levar 1-2 minutos. Tente novamente em breve.'
}, 503);
}
const errorText = await response.text();
console.error('Erro da API:', response.status, errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
// Resposta no formato OpenAI Chat Completions
const data = await response.json() as any;
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
console.error('Formato de resposta inesperado:', JSON.stringify(data).substring(0, 200));
throw new Error('Resposta inválida da API');
}
const generatedText = data.choices[0].message.content;
// Limpar o texto da resposta removendo markdown e tokens especiais
let cleanedText = generatedText
.trim()
.replace(/```json\s*/g, '')
.replace(/```\s*/g, '')
.replace(/^\s*\[/m, '[') // Remove espaços antes do array
.trim();
// Tentar encontrar o array JSON na resposta
const jsonMatch = cleanedText.match(/\[[\s\S]*\]/);
if (jsonMatch) {
cleanedText = jsonMatch[0];
}
// Parse do JSON
const generatedCards = JSON.parse(cleanedText);
// Validar o formato
if (!Array.isArray(generatedCards) || generatedCards.length === 0) {
throw new Error('O modelo não retornou cards válidos');
}
// Validar cada card
const validCards = generatedCards.filter(card =>
card && typeof card === 'object' && card.front && card.back
);
if (validCards.length === 0) {
throw new Error('Nenhum card válido foi gerado');
}
return c.json(validCards, 200);
} catch (error: any) {
console.error("Erro ao gerar cards:", error.message);
if (error instanceof SyntaxError) {
return c.json({
error: 'Erro ao processar a resposta do modelo. O formato JSON está inválido.'
}, 500);
}
return c.json({
error: error.message || 'Não foi possível gerar os cards com o GPT-OSS.'
}, 500);
}
});
// --- Iniciar o Servidor ---
export default {
port: PORT,
fetch: app.fetch,
};
console.log(`🚀 Servidor backend rodando em http://localhost:${PORT}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment