This commit is contained in:
2026-03-22 13:01:46 -03:00
parent af0910f428
commit 6694bce736
52 changed files with 4949 additions and 102 deletions

9
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

81
src/lib/related.ts Normal file
View File

@@ -0,0 +1,81 @@
import { prisma } from '@/lib/prisma'
interface ScoredNote {
id: string
title: string
type: string
tags: string[]
score: number
reason: string
}
export async function getRelatedNotes(noteId: string, limit = 5): Promise<ScoredNote[]> {
const note = await prisma.note.findUnique({
where: { id: noteId },
include: { tags: { include: { tag: true } } },
})
if (!note) return []
const noteTagNames = note.tags.map(t => t.tag.name)
const noteWords = note.title.toLowerCase().split(/\s+/).filter(w => w.length > 2)
const noteContentWords = note.content.toLowerCase().split(/\s+/).filter(w => w.length > 4)
const allNotes = await prisma.note.findMany({
where: { id: { not: noteId } },
include: { tags: { include: { tag: true } } },
})
const scored: ScoredNote[] = []
for (const other of allNotes) {
let score = 0
const reasons: string[] = []
// +3 si comparten tipo
if (other.type === note.type) {
score += 3
reasons.push(`Same type (${note.type})`)
}
// +2 por cada tag compartido
const sharedTags = noteTagNames.filter(t => other.tags.some(ot => ot.tag.name === t))
score += sharedTags.length * 2
if (sharedTags.length > 0) {
reasons.push(`Shared tags: ${sharedTags.join(', ')}`)
}
// +1 por palabra relevante compartida en título
const sharedTitleWords = noteWords.filter(w =>
other.title.toLowerCase().includes(w)
)
score += Math.min(sharedTitleWords.length, 2) // max +2
if (sharedTitleWords.length > 0) {
reasons.push(`Title match: ${sharedTitleWords.slice(0, 2).join(', ')}`)
}
// +1 si keyword del contenido aparece en ambas
const sharedContentWords = noteContentWords.filter(w =>
other.content.toLowerCase().includes(w)
)
score += Math.min(sharedContentWords.length, 2) // max +2
if (sharedContentWords.length > 0) {
reasons.push(`Content: ${sharedContentWords.slice(0, 2).join(', ')}`)
}
if (score > 0) {
scored.push({
id: other.id,
title: other.title,
type: other.type,
tags: other.tags.map(t => t.tag.name),
score,
reason: reasons.join(' | '),
})
}
}
return scored
.sort((a, b) => b.score - a.score)
.slice(0, limit)
}

24
src/lib/tags.ts Normal file
View File

@@ -0,0 +1,24 @@
const TAG_KEYWORDS: Record<string, string[]> = {
code: ['code', 'function', 'class', 'algorithm', 'programming', 'javascript', 'typescript', 'python', 'react'],
bash: ['bash', 'shell', 'command', 'terminal', 'script', 'cli'],
sql: ['sql', 'database', 'query', 'table', 'select', 'insert'],
cocina: ['receta', 'cocina', 'comida', 'horno', 'sartén', 'ingrediente'],
hogar: ['casa', 'hogar', 'inventario', 'almacen', 'cocina', 'baño'],
arquitectura: ['arquitectura', 'design', 'pattern', 'system', 'microservice', 'api'],
backend: ['backend', 'server', 'database', 'api', 'endpoint'],
frontend: ['frontend', 'ui', 'react', 'component', 'css', 'tailwind'],
devops: ['docker', 'kubernetes', 'deploy', 'ci/cd', 'pipeline', 'cloud'],
}
export function suggestTags(title: string, content: string): string[] {
const text = `${title} ${content}`.toLowerCase()
const suggested: string[] = []
for (const [tag, keywords] of Object.entries(TAG_KEYWORDS)) {
if (keywords.some(keyword => text.includes(keyword))) {
suggested.push(tag)
}
}
return suggested.slice(0, 3)
}

60
src/lib/templates.ts Normal file
View File

@@ -0,0 +1,60 @@
export const templates: Record<string, string> = {
command: `## Comando
## Qué hace
## Cuándo usarlo
## Ejemplo
\`\`\`bash
\`\`\`
`,
snippet: `## Snippet
## Lenguaje
## Qué resuelve
## Notas
`,
decision: `## Contexto
## Decisión
## Alternativas consideradas
## Consecuencias
`,
recipe: `## Ingredientes
## Pasos
## Tiempo
## Notas
`,
procedure: `## Objetivo
## Pasos
## Requisitos
## Problemas comunes
`,
inventory: `## Item
## Cantidad
## Ubicación
## Notas
`,
note: `## Notas
`,
}
export function getTemplate(type: string): string {
return templates[type] || templates.note
}

26
src/lib/validators.ts Normal file
View File

@@ -0,0 +1,26 @@
import { z } from 'zod'
export const NoteTypeEnum = z.enum(['command', 'snippet', 'decision', 'recipe', 'procedure', 'inventory', 'note'])
export const noteSchema = z.object({
id: z.string().optional(),
title: z.string().min(1, 'Title is required').max(200),
content: z.string().min(1, 'Content is required'),
type: NoteTypeEnum.default('note'),
isFavorite: z.boolean().default(false),
isPinned: z.boolean().default(false),
tags: z.array(z.string()).optional(),
})
export const updateNoteSchema = noteSchema.partial().extend({
id: z.string(),
})
export const searchSchema = z.object({
q: z.string().optional(),
type: NoteTypeEnum.optional(),
tag: z.string().optional(),
})
export type NoteInput = z.infer<typeof noteSchema>
export type UpdateNoteInput = z.infer<typeof updateNoteSchema>