Created
October 6, 2025 17:06
-
-
Save brunoksato/94f43a17f8186edc39c0a477e64d2eeb to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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