mvp
This commit is contained in:
9
src/lib/prisma.ts
Normal file
9
src/lib/prisma.ts
Normal 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
81
src/lib/related.ts
Normal 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
24
src/lib/tags.ts
Normal 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
60
src/lib/templates.ts
Normal 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
26
src/lib/validators.ts
Normal 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>
|
||||
Reference in New Issue
Block a user