feat: improve related notes algorithm and add seed data

- Add multilingual stop words (English + Spanish) for better matching
- Add technical keywords set for relevance scoring
- Improve scoring weights: tags +3, title matches +3
- Fix false positives between unrelated notes
- Add README with usage instructions
- Add 47 seed examples for testing
- Update quick add shortcut behavior
- Add project summary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 15:09:20 -03:00
parent 8b77c7b5df
commit cc4b2453b1
6 changed files with 524 additions and 26 deletions

View File

@@ -24,6 +24,6 @@ Build, test, and lint commands will be documented here once the project structur
## Resumen ## Resumen
- Cuando te pida realizar un resumen del proyecto debes crear un archivo con el siguiente formato de nombre yyyy-mm-dd-resumen.md en la carpeta resumen. - Cuando te pida realizar un resumen del proyecto debes crear un archivo con el siguiente formato de nombre yyyy-mm-dd-HHMM-resumen.md en la carpeta resumen.
- Si no existe crea una carpeta resumen en la raiz del proyecto. - Si no existe crea una carpeta resumen en la raiz del proyecto.
- Crearemos resumenes de forma incremental y el primero debe contener todo lo existente hasta el momento. - Crearemos resumenes de forma incremental y el primero debe contener todo lo existente hasta el momento.

View File

@@ -1,2 +1,89 @@
# recall # recall
Sistema de notas personal con captura rápida y búsqueda inteligente.
## Uso
### Quick Add (Captura Rápida)
Crea notas al instante con el shortcut `Ctrl+N`.
Sintaxis:
```
[tipo:][título] #tag1 #tag2
```
**Tipos disponibles:**
- `cmd:` - Comando
- `snip:` - Snippet de código
- `dec:` - Decisión
- `rec:` - Receta
- `proc:` - Procedimiento
- `inv:` - Inventario
**Ejemplos:**
```
cmd: git commit -m 'fix: bug' #git #version-control
snip: useState hook #react #hooks
dec: usar PostgreSQL #backend #database
rec: Pasta carbonara #cocina #italiana
```
### Tipos de Notas
| Tipo | Descripción | Campos |
|------|-------------|--------|
| `command` | Comandos CLI | Comando, Descripción, Ejemplo |
| `snippet` | Código reutilizable | Lenguaje, Código, Descripción |
| `decision` | Decisiones importantes | Contexto, Decisión, Alternativas, Consecuencias |
| `recipe` | Recetas | Ingredientes, Pasos, Tiempo |
| `procedure` | Procedimientos | Objetivo, Pasos, Requisitos |
| `inventory` | Inventario | Item, Cantidad, Ubicación |
| `note` | Nota libre | Contenido |
### Búsqueda
- Búsqueda por título y contenido
- Búsqueda fuzzy (tolerante a errores)
- Filtros por tipo y tags
- Favoritos y notas pinned influyen en el ranking
### Links entre Notas
Crea links a otras notas usando `[[nombre-de-nota]]`:
```
Ver también: [[Configuración de Docker]]
```
Los backlinks se muestran automáticamente en la nota referenciada.
## Development
```bash
npm install
npx prisma db push
npm run dev
```
## API
### Quick Add
```bash
POST /api/notes/quick
Content-Type: text/plain
cmd: mi comando #tag
```
### Buscar
```bash
GET /api/search?q=docker&type=command
```
### Tags
```bash
GET /api/tags # Listar todos
GET /api/tags?q=python # Filtrar
GET /api/tags/suggest?title=...&content=... # Sugerencias
```

118
prisma/seed-examples.ts Normal file
View File

@@ -0,0 +1,118 @@
/**
* Seed script con 50 ejemplos para probar búsquedas
* Usage: npx tsx prisma/seed-examples.ts
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const examples = [
// Commands
{ type: 'command', title: 'Git commit with message', content: '## Comando\n\ngit commit -m "fix: resolve bug"\n\n## Qué hace\n\nCreates a commit with a message\n\n## Ejemplo\n\n```bash\ngit commit -m "feat: add new feature"\n```', tags: ['git', 'version-control'] },
{ type: 'command', title: 'Docker remove all containers', content: '## Comando\n\ndocker rm -f $(docker ps -aq)\n\n## Qué hace\n\nForce removes all containers\n\n## Ejemplo\n\n```bash\ndocker rm -f $(docker ps -aq)\n```', tags: ['docker', 'devops'] },
{ type: 'command', title: 'Find files modified today', content: '## Comando\n\nfind . -type f -mtime 0\n\n## Qué hace\n\nFinds files modified in the last 24 hours\n\n## Ejemplo\n\n```bash\nfind . -type f -mtime -1\n```', tags: ['bash', 'shell'] },
{ type: 'command', title: 'Kill process on port', content: '## Comando\n\nlsof -ti:<port> | xargs kill -9\n\n## Qué hace\n\nKills process running on specified port\n\n## Ejemplo\n\n```bash\nlsof -ti:3000 | xargs kill -9\n```', tags: ['bash', 'network'] },
{ type: 'command', title: 'Git undo last commit', content: '## Comando\n\ngit reset --soft HEAD~1\n\n## Qué hace\n\nUndoes the last commit keeping changes staged\n\n## Ejemplo\n\n```bash\ngit reset --soft HEAD~1\n```', tags: ['git'] },
{ type: 'command', title: 'NPM install specific version', content: '## Comando\n\nnpm install <package>@<version>\n\n## Qué hace\n\nInstalls a specific version of a package\n\n## Ejemplo\n\n```bash\nnpm install lodash@4.17.21\n```', tags: ['npm', 'node'] },
{ type: 'command', title: 'Rsync with ssh', content: '## Comando\n\nrsync -avz -e ssh source/ user@host:dest/\n\n## Qué hace\n\nSyncs files via SSH\n\n## Ejemplo\n\n```bash\nrsync -avz -e ssh ./data/ user@server:/backup/\n```', tags: ['bash', 'network'] },
// Snippets
{ type: 'snippet', title: 'React useState hook', content: '## Snippet\n\n## Lenguaje\n\ntypescript\n\n## Código\n\n```typescript\nconst [state, setState] = useState(initialValue)\n```\n\n## Descripción\n\nBasic React state hook usage', tags: ['react', 'hooks'] },
{ type: 'snippet', title: 'Python list comprehension', content: '## Snippet\n\n## Lenguaje\n\npython\n\n## Código\n\n```python\nsquares = [x**2 for x in range(10)]\n```\n\n## Descripción\n\nCreate list of squares using comprehension', tags: ['python'] },
{ type: 'snippet', title: 'CSS flexbox centering', content: '## Snippet\n\n## Lenguaje\n\ncss\n\n## Código\n\n```css\ndisplay: flex;\njustify-content: center;\nalign-items: center;\n```\n\n## Descripción\n\nCenter element horizontally and vertically', tags: ['css', 'flexbox'] },
{ type: 'snippet', title: 'JavaScript async await', content: '## Snippet\n\n## Lenguaje\n\njavascript\n\n## Código\n\n```javascript\nconst result = await fetchData()\nconsole.log(result)\n```\n\n## Descripción\n\nAsync/await pattern for Promises', tags: ['javascript', 'async'] },
{ type: 'snippet', title: 'SQL SELECT with JOIN', content: '## Snippet\n\n## Lenguaje\n\nsql\n\n## Código\n\n```sql\nSELECT n.*, t.name as tag\nFROM notes n\nJOIN note_tags nt ON n.id = nt.note_id\nJOIN tags t ON nt.tag_id = t.id\n```\n\n## Descripción\n\nJoin notes with tags', tags: ['sql', 'database'] },
{ type: 'snippet', title: 'Go error handling', content: '## Snippet\n\n## Lenguaje\n\ngo\n\n## Código\n\n```go\nif err != nil {\n return fmt.Errorf("failed: %w", err)\n}\n```\n\n## Descripción\n\nStandard Go error handling with wrapping', tags: ['go'] },
{ type: 'snippet', title: 'Bash function', content: '## Snippet\n\n## Lenguaje\n\nbash\n\n## Código\n\n```bash\nfunction greet() {\n echo "Hello, $1!"\n}\n```\n\n## Descripción\n\nBasic bash function with parameter', tags: ['bash', 'shell'] },
// Decisions
{ type: 'decision', title: 'Use PostgreSQL over MySQL', content: '## Contexto\n\nNeed a relational database for the backend API\n\n## Decisión\n\nChose PostgreSQL for its advanced features\n\n## Alternativas consideradas\n\nMySQL - simpler but less features\nSQLite - embedded, not for production\n\n## Consecuencias\n\nBetter data integrity, JSON support, Full-text search built-in', tags: ['database', 'backend', 'postgresql'] },
{ type: 'decision', title: 'Use Next.js App Router', content: '## Contexto\n\nStarting a new full-stack project\n\n## Decisión\n\nNext.js with App Router for server components\n\n## Alternativas consideradas\n\nExpress - more control but manual setup\nRemix - good but smaller ecosystem\n\n## Consecuencias\n\nBetter SEO, easier deployment, React Server Components', tags: ['frontend', 'react', 'architecture'] },
{ type: 'decision', title: 'Use TypeScript strict mode', content: '## Contexto\n\nSetting up a new TypeScript project\n\n## Decisión\n\nEnable strict mode from the start\n\n## Alternativas consideradas\n\nDisable strict initially - leads to tech debt\n\n## Consecuencias\n\nCatches errors early, better code quality', tags: ['typescript', 'code-quality'] },
{ type: 'decision', title: 'Use Tailwind CSS', content: '## Contexto\n\nNeed a styling solution for React project\n\n## Decisión\n\nTailwind CSS for rapid development\n\n## Alternativas consideradas\n\nStyled Components - runtime overhead\nCSS Modules - more setup\n\n## Consecuencias\n\nFaster development, consistent design, smaller bundle', tags: ['css', 'frontend'] },
// Recipes
{ type: 'recipe', title: 'Pasta carbonara', content: '## Ingredientes\n\n- 400g spaghetti\n- 200g guanciale\n- 4 egg yolks\n- 100g pecorino romano\n- Black pepper\n\n## Pasos\n\n1. Cook pasta in salted water\n2. Cut guanciale into cubes\n3. Fry guanciale until crispy\n4. Mix egg yolks with cheese\n5. Combine hot pasta with guanciale\n6. Add egg mixture off heat\n7. Toss until creamy\n\n## Tiempo\n\n20 minutes', tags: ['cocina', 'italiana', 'pasta'] },
{ type: 'recipe', title: 'Guacamole', content: '## Ingredientes\n\n- 3 avocados\n- 1 lime\n- 1 onion\n- 2 tomatoes\n- Cilantro\n- Salt\n\n## Pasos\n\n1. Cut avocados and mash\n2. Dice onion and tomatoes finely\n3. Add lime juice\n4. Chop cilantro\n5. Mix all ingredients\n6. Season to taste\n\n## Tiempo\n\n10 minutes', tags: ['cocina', 'mexicana'] },
{ type: 'recipe', title: 'Chicken stir fry', content: '## Ingredientes\n\n- 500g chicken breast\n- 2 bell peppers\n- 1 broccoli\n- Soy sauce\n- Ginger\n- Garlic\n\n## Pasos\n\n1. Cut chicken into strips\n2. Chop vegetables\n3. Stir fry chicken until cooked\n4. Add vegetables\n5. Add soy sauce and spices\n6. Serve with rice\n\n## Tiempo\n\n25 minutes', tags: ['cocina', 'asiatica'] },
// Procedures
{ type: 'procedure', title: 'Deploy to Vercel', content: '## Objetivo\n\nDeploy Next.js app to Vercel production\n\n## Pasos\n\n1. Push changes to main branch\n2. Wait for CI to pass\n3. Go to Vercel dashboard\n4. Click Deployments\n5. Select latest deployment\n6. Click Promote to Production\n7. Verify with production URL\n\n## Requisitos\n\n- Vercel account connected\n- GitHub repository linked\n- Production branch protected', tags: ['devops', 'deployment', 'vercel'] },
{ type: 'procedure', title: 'Setup Git hooks', content: '## Objetivo\n\nConfigure pre-commit hooks with husky\n\n## Pasos\n\n1. Install husky: npm install husky\n2. Init: npx husky install\n3. Add pre-commit: npx husky add .husky/pre-commit "npx lint-staged"\n4. Configure lint-staged in package.json\n5. Test by committing\n\n## Requisitos\n\n- Git repository initialized\n- Package.json configured', tags: ['git', 'devops'] },
{ type: 'procedure', title: 'Database backup', content: '## Objetivo\n\nCreate a backup of the production database\n\n## Pasos\n\n1. Connect to production server\n2. Run pg_dump command\n3. Save to timestamped file\n4. Copy to backup storage\n5. Verify backup integrity\n6. Delete backups older than 30 days\n\n## Requisitos\n\n- SSH access to server\n- Sufficient disk space\n- Backup storage configured', tags: ['database', 'devops', 'backup'] },
{ type: 'procedure', title: 'Code review process', content: '## Objetivo\n\nStandardize code review workflow\n\n## Pasos\n\n1. Create feature branch\n2. Write code and tests\n3. Open PR with description\n4. Request review from teammate\n5. Address feedback\n6. Get approval\n7. Squash and merge\n\n## Requisitos\n\n- GitHub configured\n- CI passing\n- At least 1 approval', tags: ['git', 'process'] },
// Inventory
{ type: 'inventory', title: 'Laptop specs', content: '## Item\n\nMacBook Pro 16" 2023\n\n## Cantidad\n\n1\n\n## Ubicación\n\nHome office desk\n\n## Notas\n\n- M3 Pro chip\n- 18GB RAM\n- 512GB SSD\n- Serial: ABC123XYZ', tags: ['laptop', 'hardware'] },
{ type: 'inventory', title: 'Cable management', content: '## Item\n\nUSB-C cables\n\n## Cantidad\n\n5\n\n## Ubicación\n\nDesk drawer\n\n## Notas\n\n3x 1m, 2x 2m braided', tags: ['cables', 'hardware'] },
{ type: 'inventory', title: 'Office supplies', content: '## Item\n\nNotebooks\n\n## Cantidad\n\n8\n\n## Ubicación\n\nBookshelf\n\n## Notas\n\nVarious sizes, mostly unused', tags: ['office'] },
// Notes with various topics
{ type: 'note', title: 'Project ideas', content: '## Notas\n\n- Build a weather app with geolocation\n- Create a recipe manager\n- Design a habit tracker\n- Develop a markdown editor', tags: ['ideas', 'projects'] },
{ type: 'note', title: 'Books to read', content: '## Notas\n\n1. Clean Code - Robert Martin\n2. Design Patterns - Gang of Four\n3. The Pragmatic Programmer\n4. Domain-Driven Design', tags: ['reading', 'books'] },
{ type: 'note', title: 'Conference notes', content: '## Notas\n\nKey takeaways from React Conf:\n- Server Components are the future\n- Suspense for streaming\n- Better error boundaries', tags: ['conference', 'react'] },
{ type: 'note', title: 'Docker compose template', content: '## Notas\n\nBasic docker-compose.yml structure for web apps:\n- App service\n- Database service\n- Redis service\n- Nginx reverse proxy', tags: ['docker', 'devops'] },
{ type: 'note', title: 'API design principles', content: '## Notas\n\n- RESTful conventions\n- Use nouns, not verbs\n- Version your APIs\n- Return proper status codes\n- Pagination for lists', tags: ['api', 'backend'] },
{ type: 'note', title: 'Git branching strategy', content: '## Notas\n\n- main: production\n- develop: staging\n- feature/*: new features\n- bugfix/*: fixes\n- hotfix/*: urgent production fixes', tags: ['git', 'process'] },
{ type: 'note', title: 'Keyboard shortcuts VSCode', content: '## Notas\n\n- Cmd+D: Select next occurrence\n- Cmd+Shift+L: Select all occurrences\n- Cmd+P: Quick open file\n- Cmd+Shift+P: Command palette', tags: ['vscode', 'productivity'] },
{ type: 'note', title: 'CSS Grid cheatsheet', content: '## Notas\n\ngrid-template-columns: repeat(3, 1fr)\ngrid-gap: 1rem\nplace-items: center\ngrid-area: header / sidebar / content / footer', tags: ['css', 'grid'] },
{ type: 'note', title: 'React hooks cheatsheet', content: '## Notas\n\n- useState: local state\n- useEffect: side effects\n- useCallback: memoize function\n- useMemo: memoize value\n- useRef: mutable ref', tags: ['react', 'hooks'] },
{ type: 'note', title: 'Linux commands cheatsheet', content: '## Notas\n\n- chmod +x: make executable\n- chown user:group: change owner\n- grep -r: recursive search\n- tar -czvf: compress\n- ssh -i key: connect with key', tags: ['bash', 'linux'] },
{ type: 'note', title: 'SQL joins explained', content: '## Notas\n\n- INNER: matching in both\n- LEFT: all from left + matching from right\n- RIGHT: all from right + matching from left\n- FULL: all from both\n- CROSS: Cartesian product', tags: ['sql', 'database'] },
{ type: 'note', title: 'TypeScript utility types', content: '## Notas\n\n- Partial: all optional\n- Required: all required\n- Pick: select fields\n- Omit: exclude fields\n- Record: key-value object\n- Exclude: remove from union', tags: ['typescript'] },
{ type: 'note', title: 'Testing pyramid', content: '## Notas\n\n- Unit tests: base, many\n- Integration tests: some\n- E2E tests: few, slow, expensive', tags: ['testing', 'quality'] },
{ type: 'note', title: 'Markdown syntax', content: '## Notas\n\n# Heading\n## Subheading\n**bold** *italic*\n- list\n1. numbered\n[link](url)\n```code block```', tags: ['markdown'] },
{ type: 'note', title: 'Docker vs Kubernetes', content: '## Notas\n\nDocker: containerize apps\nKubernetes: orchestrate containers at scale\n\nDocker Compose: local multi-container\nK8s: production container orchestration', tags: ['docker', 'kubernetes', 'devops'] },
{ type: 'note', title: 'JWT structure', content: '## Notas\n\nHeader: algorithm, type\nPayload: claims, exp, iss\nSignature: verify authenticity\n\nNever store sensitive data in JWT payload', tags: ['auth', 'security', 'jwt'] },
{ type: 'note', title: 'Web security headers', content: '## Notas\n\n- Content-Security-Policy\n- X-Frame-Options\n- X-Content-Type-Options\n- Strict-Transport-Security\n- CORS', tags: ['security', 'web'] },
{ type: 'note', title: 'Redis use cases', content: '## Notas\n\n- Session storage\n- Caching\n- Rate limiting\n- Pub/Sub\n- Leaderboards', tags: ['redis', 'database'] },
{ type: 'note', title: 'Git squash commits', content: '## Notas\n\n```bash\ngit rebase -i HEAD~3\n```\n\nChange pick to squash for commits to combine', tags: ['git'] },
]
async function main() {
console.log('Seeding database with examples...')
// Clear existing notes
await prisma.noteTag.deleteMany()
await prisma.note.deleteMany()
await prisma.tag.deleteMany()
for (const note of examples) {
// Create or get tags
const tagRecords = await Promise.all(
note.tags.map(async (tagName) => {
return prisma.tag.upsert({
where: { name: tagName },
create: { name: tagName },
update: {},
})
})
)
// Create note
await prisma.note.create({
data: {
title: note.title,
content: note.content,
type: note.type,
tags: {
create: tagRecords.map((tag) => ({ tagId: tag.id })),
},
},
})
console.log(` Created: ${note.title}`)
}
console.log(`\nSeeded ${examples.length} notes successfully!`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

View File

@@ -0,0 +1,207 @@
# Resumen del Proyecto - 2026-03-22
## Nombre
**Recall** - Sistema de gestión de notas personales
## Descripción
Aplicación web para crear, editar, buscar y organizar notas personales con soporte para tags, tipos de notas, favoritos, pins, captura rápida, backlinks y relaciones entre notas.
---
## Tech Stack
| Categoría | Tecnología |
|-----------|------------|
| Framework | Next.js 16.2.1 (React 19.2.4) |
| Base UI | @base-ui/react 1.3.0 |
| Database | SQLite con Prisma ORM |
| Validation | Zod 4.3.6 |
| Styling | Tailwind CSS 4 + CSS Variables |
| Icons | Lucide React |
| Markdown | react-markdown + remark-gfm |
| Syntax Highlight | react-syntax-highlighter |
| Toast | sonner 2.0.7 |
| Testing | Jest |
---
## Estructura del Proyecto
```
src/
├── app/
│ ├── api/
│ │ ├── export-import/route.ts
│ │ ├── notes/
│ │ │ ├── route.ts
│ │ │ ├── [id]/route.ts
│ │ │ │ ├── backlinks/route.ts
│ │ │ │ └── route.ts
│ │ │ ├── quick/route.ts
│ │ │ └── suggest/route.ts
│ │ ├── search/route.ts
│ │ └── tags/
│ │ ├── route.ts
│ │ └── suggest/route.ts
│ ├── notes/[id]/page.tsx
│ ├── notes/page.tsx
│ ├── new/page.tsx
│ ├── edit/[id]/page.tsx
│ ├── settings/page.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ui/ # shadcn/ui components
│ ├── dashboard.tsx
│ ├── header.tsx
│ ├── markdown-content.tsx # Markdown con syntax highlight
│ ├── note-card.tsx
│ ├── note-form.tsx # Form con campos guiados
│ ├── note-list.tsx
│ ├── quick-add.tsx # Ctrl+N quick add
│ ├── related-notes.tsx
│ ├── search-bar.tsx
│ ├── delete-note-button.tsx
│ └── tag-filter.tsx
├── lib/
│ ├── prisma.ts
│ ├── utils.ts
│ ├── validators.ts
│ ├── errors.ts # Error handling estándar
│ ├── search.ts # Búsqueda con scoring
│ ├── quick-add.ts # Parser para captura rápida
│ ├── tags.ts # Normalización y sugerencias
│ ├── templates.ts # Plantillas por tipo
│ ├── related.ts # Notas relacionadas
│ ├── backlinks.ts # Sistema de backlinks
│ └── guided-fields.ts # Campos guiados por tipo
└── types/
└── note.ts
```
---
## Modelo de Datos (Prisma)
### Note
| Campo | Tipo | Descripción |
|-------|------|-------------|
| id | String | CUID único |
| title | String | Título |
| content | String | Contenido en Markdown |
| type | String | command, snippet, decision, recipe, procedure, inventory, note |
| isFavorite | Boolean | Favorita |
| isPinned | Boolean | Fijada |
| createdAt | DateTime | Creación |
| updatedAt | DateTime | Última modificación |
### Tag
| Campo | Tipo | Descripción |
|-------|------|-------------|
| id | String | CUID único |
| name | String | Nombre único (lowercase) |
### Backlink
Relación bidireccional entre notas via `[[nombre-nota]]`
---
## APIs
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/api/notes` | Listar notas |
| POST | `/api/notes` | Crear nota |
| GET | `/api/notes/[id]` | Obtener nota |
| PUT | `/api/notes/[id]` | Actualizar nota |
| DELETE | `/api/notes/[id]` | Eliminar nota |
| GET | `/api/notes/[id]/backlinks` | Backlinks recibidos |
| POST | `/api/notes/quick` | Captura rápida |
| GET | `/api/notes/suggest` | Sugerencias de notas |
| GET | `/api/search` | Búsqueda con scoring |
| GET | `/api/tags` | Listar/sugerir tags |
| GET | `/api/tags/suggest` | Sugerencias por contenido |
| GET/POST | `/api/export-import` | Exportar/importar |
---
## Funcionalidades Implementadas
### 1. Captura Rápida (Quick Add)
- Shortcut global `Ctrl+N`
- Sintaxis: `[tipo:][título] #tag1 #tag2`
- Tipos: `cmd:`, `snip:`, `dec:`, `rec:`, `proc:`, `inv:`
- API: `POST /api/notes/quick`
### 2. Búsqueda y Recuperación
- Scoring: título exacto > parcial > favoritos > pinned > recencia
- Búsqueda fuzzy (tolerante a errores de escritura)
- Resaltado de términos con excerpt
- Filtros por tipo y tags
### 3. Relaciones entre Notas
- **Backlinks automáticos:** detecta `[[nombre-nota]]`
- **Notas relacionadas:** scoring por tags, tipo, palabras
- API: `GET /api/notes/[id]/backlinks`
### 4. Campos Guiados por Tipo
- Command: comando, descripción, ejemplo
- Snippet: lenguaje, código, descripción
- Decision: contexto, decisión, alternativas, consecuencias
- Recipe: ingredientes, pasos, tiempo
- Procedure: objetivo, pasos, requisitos
- Inventory: item, cantidad, ubicación
### 5. UX por Tipo
- **Command:** botón copiar
- **Snippet:** syntax highlighting
- **Procedure:** checkboxes interactivos
### 6. Sistema de Tags
- Normalización automática (lowercase, trim)
- Autocomplete en formularios
- Sugerencias basadas en contenido
---
## Tests
```bash
npx jest __tests__/
# 46 passing, 3 skipped
```
---
## Comandos
```bash
npm install # Instalar dependencias
npx prisma db push # Sincronizar schema con BD
npm run dev # Desarrollo (localhost:3000)
npm run build # Build producción
```
---
## Rutas de la Aplicación
| Ruta | Descripción |
|------|-------------|
| `/` | Dashboard |
| `/notes` | Lista de notas |
| `/notes/[id]` | Detalle de nota |
| `/new` | Crear nota |
| `/edit/[id]` | Editar nota |
| `/settings` | Exportar/importar |
---
## Notas Técnicas
- App Router (Next.js 13+)
- Server Components para datos
- Client Components para interactividad
- Prisma con SQLite
- Zod para validación
- Errores API con formato `{ success, data, error, timestamp }`

View File

@@ -62,11 +62,18 @@ export function QuickAdd() {
// Focus on keyboard shortcut // Focus on keyboard shortcut
useEffect(() => { useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => { const handleGlobalKeyDown = (e: KeyboardEvent) => {
if (e.key === 'n' && (e.metaKey || e.ctrlKey)) { // Ctrl+N or Cmd+N to focus quick add
if ((e.key === 'n' && (e.metaKey || e.ctrlKey)) || (e.key === 'n' && e.altKey)) {
e.preventDefault() e.preventDefault()
inputRef.current?.focus() inputRef.current?.focus()
inputRef.current?.select()
setIsExpanded(true) setIsExpanded(true)
} }
// Escape to blur
if (e.key === 'Escape' && document.activeElement === inputRef.current) {
inputRef.current?.blur()
setIsExpanded(false)
}
} }
window.addEventListener('keydown', handleGlobalKeyDown) window.addEventListener('keydown', handleGlobalKeyDown)
return () => window.removeEventListener('keydown', handleGlobalKeyDown) return () => window.removeEventListener('keydown', handleGlobalKeyDown)

View File

@@ -1,5 +1,81 @@
import { prisma } from '@/lib/prisma' import { prisma } from '@/lib/prisma'
// Stop words to filter out from content matching (English + Spanish)
const STOP_WORDS = new Set([
// English
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought',
'used', 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'you', 'he',
'she', 'we', 'they', 'what', 'which', 'who', 'whom', 'whose', 'where',
'when', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more',
'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own',
'same', 'so', 'than', 'too', 'very', 'just', 'also', 'now', 'here',
'there', 'then', 'once', 'if', 'your', 'our', 'their', 'my', 'his',
'her', 'into', 'over', 'under', 'after', 'before', 'between', 'through',
'during', 'above', 'below', 'up', 'down', 'out', 'off', 'about', 'against',
'config', 'file', 'files', 'using', 'use', 'example', 'following', 'etc',
'based', 'include', 'includes', 'included', 'add', 'added', 'adding',
'see', 'want', 'make', 'made', 'creating', 'create', 'created',
// Spanish
'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas', 'y', 'o', 'pero',
'en', 'de', 'a', 'con', 'por', 'para', 'sin', 'sobre', 'entre', 'del',
'al', 'lo', 'se', 'es', 'son', 'era', 'eran', 'fue', 'fueron', 'ser',
'estar', 'está', 'están', 'estaba', 'estaban', 'he', 'ha', 'han', 'hay',
'haber', 'había', 'habían', 'tener', 'tiene', 'tienen', 'tenía', 'hacer',
'hace', 'hacen', 'hizo', 'hicieron', 'poder', 'puede', 'pueden', 'podía',
'este', 'esta', 'estos', 'estas', 'ese', 'esa', 'esos', 'esas', 'esto',
'eso', 'cual', 'cuales', 'quien', 'quienes', 'cuyo', 'cuyos', 'donde',
'cuando', 'como', 'porque', 'ya', 'aun', 'aunque', 'si', 'no', 'ni',
'mi', 'tu', 'su', 'sus', 'nuestro', 'nuestra', 'nuestros', 'nuestras',
'yo', 'tú', 'él', 'ella', 'ellos', 'ellas', 'nosotros', 'vosotros',
'ustedes', 'mí', 'ti', 'sí', 'qué', 'quién', 'cuál', 'cuáles',
'cuánto', 'cuántos', 'cuánta', 'cuántas', 'dónde', 'adónde', 'de dónde',
'nada', 'nadie', 'algo', 'alguien', 'todo', 'todos', 'toda', 'todas',
'cada', 'otro', 'otra', 'otros', 'otras', 'mismo', 'misma', 'mismos',
'mismas', 'tanto', 'tanta', 'tantos', 'tantas', 'bastante', 'bastantes',
'muy', 'más', 'menos', 'mejor', 'peor', 'mucho', 'poco', 'casi', 'solo',
'solamente', 'también', 'además', 'entonces', 'ahora', 'hoy', 'aquí',
'allí', 'así', 'así', 'tan', 'qué', 'quién', 'cuál', 'ver', 'vez',
'parte', 'parte', 'manera', 'forma', 'caso', 'casos', 'momento', 'lugar',
'día', 'días', 'año', 'años', 'mes', 'meses', 'semana', 'semanas',
'hora', 'horas', 'minuto', 'minutos', 'segundo', 'segundos',
// Common tech words that cause false positives
'command', 'comando', 'description', 'descripción', 'description', 'nota',
'notes', 'notas', 'content', 'contenido', 'code', 'código', 'ejemplo',
'example', 'steps', 'pasos', 'item', 'items', 'quantity', 'cantidad',
'añadir', 'agregar', 'nuevo', 'nueva', 'nuevos', 'nuevas', 'nueces',
])
// Keywords that indicate actual relevance (technical terms)
const KEYWORDS = new Set([
'git', 'docker', 'react', 'typescript', 'javascript', 'python', 'sql',
'postgres', 'postgresql', 'mysql', 'redis', 'nginx', 'kubernetes', 'k8s',
'api', 'http', 'json', 'xml', 'html', 'css', 'node', 'nodejs', 'npm',
'bash', 'shell', 'linux', 'ubuntu', 'aws', 'gcp', 'azure', 'vercel',
'prisma', 'nextjs', 'next', 'tailwind', 'eslint', 'prettier', 'jest',
'database', 'db', 'server', 'client', 'frontend', 'backend', 'fullstack',
'crud', 'rest', 'graphql', 'websocket', 'ssh', 'ssl', 'tls', 'jwt',
'auth', 'authentication', 'authorization', 'cookie', 'session', 'cache',
'deploy', 'deployment', 'ci', 'cd', 'pipeline', 'docker-compose',
'container', 'image', 'build', 'test', 'production', 'staging', 'dev',
'development', 'development', 'environment', 'config', 'configuration',
'variable', 'env', 'secret', 'key', 'password', 'token',
])
function extractKeywords(text: string): string[] {
const words = text.toLowerCase()
.split(/[\s\-_.,;:!?()\[\]{}'"]+/)
.filter(w => w.length > 2)
return words.filter(w => !STOP_WORDS.has(w))
}
function getSignificantWords(words: string[]): string[] {
return words.filter(w => KEYWORDS.has(w) || w.length > 4)
}
interface ScoredNote { interface ScoredNote {
id: string id: string
title: string title: string
@@ -14,56 +90,59 @@ export async function getRelatedNotes(noteId: string, limit = 5): Promise<Scored
where: { id: noteId }, where: { id: noteId },
include: { tags: { include: { tag: true } } }, include: { tags: { include: { tag: true } } },
}) })
if (!note) return [] if (!note) return []
const noteTagNames = note.tags.map(t => t.tag.name) const noteTagNames = note.tags.map(t => t.tag.name)
const noteWords = note.title.toLowerCase().split(/\s+/).filter(w => w.length > 2) const noteTitleWords = getSignificantWords(extractKeywords(note.title))
const noteContentWords = note.content.toLowerCase().split(/\s+/).filter(w => w.length > 4) const noteContentWords = getSignificantWords(extractKeywords(note.content))
const allNotes = await prisma.note.findMany({ const allNotes = await prisma.note.findMany({
where: { id: { not: noteId } }, where: { id: { not: noteId } },
include: { tags: { include: { tag: true } } }, include: { tags: { include: { tag: true } } },
}) })
const scored: ScoredNote[] = [] const scored: ScoredNote[] = []
for (const other of allNotes) { for (const other of allNotes) {
let score = 0 let score = 0
const reasons: string[] = [] const reasons: string[] = []
// +3 si comparten tipo // +3 si comparten tipo
if (other.type === note.type) { if (other.type === note.type) {
score += 3 score += 3
reasons.push(`Same type (${note.type})`) reasons.push(`Same type (${note.type})`)
} }
// +2 por cada tag compartido // +3 por cada tag compartido
const sharedTags = noteTagNames.filter(t => other.tags.some(ot => ot.tag.name === t)) const sharedTags = noteTagNames.filter(t => other.tags.some(ot => ot.tag.name === t))
score += sharedTags.length * 2 score += sharedTags.length * 3
if (sharedTags.length > 0) { if (sharedTags.length > 0) {
reasons.push(`Shared tags: ${sharedTags.join(', ')}`) reasons.push(`Tags: ${sharedTags.join(', ')}`)
} }
// +1 por palabra relevante compartida en título // +2 por palabra clave del título compartida
const sharedTitleWords = noteWords.filter(w => const otherTitleWords = extractKeywords(other.title)
other.title.toLowerCase().includes(w) const sharedTitleWords = noteTitleWords.filter(w =>
otherTitleWords.includes(w)
) )
score += Math.min(sharedTitleWords.length, 2) // max +2 score += Math.min(sharedTitleWords.length, 3) // max +3
if (sharedTitleWords.length > 0) { if (sharedTitleWords.length > 0) {
reasons.push(`Title match: ${sharedTitleWords.slice(0, 2).join(', ')}`) reasons.push(`Title: ${sharedTitleWords.slice(0, 2).join(', ')}`)
} }
// +1 si keyword del contenido aparece en ambas // +1 por palabra clave del contenido compartida
const otherContentWords = getSignificantWords(extractKeywords(other.content))
const sharedContentWords = noteContentWords.filter(w => const sharedContentWords = noteContentWords.filter(w =>
other.content.toLowerCase().includes(w) otherContentWords.includes(w)
) )
score += Math.min(sharedContentWords.length, 2) // max +2 score += Math.min(sharedContentWords.length, 2) // max +2
if (sharedContentWords.length > 0) { if (sharedContentWords.length > 0) {
reasons.push(`Content: ${sharedContentWords.slice(0, 2).join(', ')}`) reasons.push(`Content: ${sharedContentWords.slice(0, 2).join(', ')}`)
} }
if (score > 0) { // Solo incluir si tiene score > 0 Y al menos una razón válida
if (score > 0 && reasons.length > 0) {
scored.push({ scored.push({
id: other.id, id: other.id,
title: other.title, title: other.title,
@@ -74,7 +153,7 @@ export async function getRelatedNotes(noteId: string, limit = 5): Promise<Scored
}) })
} }
} }
return scored return scored
.sort((a, b) => b.score - a.score) .sort((a, b) => b.score - a.score)
.slice(0, limit) .slice(0, limit)