Skip to content

Instantly share code, notes, and snippets.

@brunoksato
Created October 6, 2025 17:06
Show Gist options
  • Select an option

  • Save brunoksato/94f43a17f8186edc39c0a477e64d2eeb to your computer and use it in GitHub Desktop.

Select an option

Save brunoksato/94f43a17f8186edc39c0a477e64d2eeb to your computer and use it in GitHub Desktop.

Revisions

  1. brunoksato created this gist Oct 6, 2025.
    286 changes: 286 additions & 0 deletions index.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,286 @@
    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}`);