feat: enrichers, panel de ingest y menu contextual en el grafo

- Añade enricher.go + directorio enrichers/ para enriquecer entidades con fuentes externas.
- Nuevos componentes frontend: IngestPanel (panel de ingesta de datos) y NodeContextMenu (menu contextual sobre nodos del grafo).
- Retira SearchBar y lib/utils.ts; la busqueda se integra dentro de los paneles existentes.
- Ajusta tipos (types.go, types.ts, wailsjs/go) y theming (postcss + app.css + Mantine).
- Actualiza app.go y wails.json para exponer las nuevas capacidades.
- Añade directorio projects/ con estado inicial.
- Rebuild del frontend (dist actualizado).
This commit is contained in:
2026-04-13 23:32:55 +02:00
parent 23198eee0c
commit c9fd4aa84c
42 changed files with 2615 additions and 1543 deletions
+51 -50
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Stack, Group, TextInput, Textarea, Text, NumberInput } from '@mantine/core'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, SimpleSelect, Button } from '@fn_library'
import type { Entity, RelationInputDTO } from '../types'
import { SimpleSelect } from '@fn_library'
import { X } from 'lucide-react'
interface Props {
entities: Entity[]
@@ -15,11 +15,11 @@ export function RelationDialog({ entities, relationPresets, onSubmit, onClose }:
const [fromEntity, setFromEntity] = useState(entities[0]?.id ?? '')
const [toEntity, setToEntity] = useState(entities[1]?.id ?? entities[0]?.id ?? '')
const [description, setDescription] = useState('')
const [weight, setWeight] = useState('1.0')
const [weight, setWeight] = useState<number | string>(1.0)
const [notes, setNotes] = useState('')
const handleSubmit = () => {
const w = parseFloat(weight)
const w = typeof weight === 'number' ? weight : parseFloat(String(weight))
onSubmit({
name,
from_entity: fromEntity,
@@ -31,23 +31,16 @@ export function RelationDialog({ entities, relationPresets, onSubmit, onClose }:
})
}
const inputStyle = {
background: 'var(--input)',
color: 'var(--foreground)',
border: '1px solid var(--border)',
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.6)' }}>
<div className="w-[480px] rounded-lg p-5" style={{ background: 'var(--card)', border: '1px solid var(--border)' }}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold">New Relation</h3>
<button onClick={onClose} className="p-1"><X size={16} style={{ color: 'var(--muted-foreground)' }} /></button>
</div>
<Dialog open onOpenChange={(open: boolean) => { if (!open) onClose() }}>
<DialogContent>
<DialogHeader>
<DialogTitle>New Relation</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<Stack gap="sm">
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Relation Type</label>
<Text size="sm" fw={500} mb={4}>Relation Type</Text>
<SimpleSelect
value={name}
onValueChange={setName}
@@ -55,53 +48,61 @@ export function RelationDialog({ entities, relationPresets, onSubmit, onClose }:
/>
</div>
<div className="flex gap-3">
<div className="flex-1">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>From</label>
<Group gap="sm" grow>
<div>
<Text size="sm" fw={500} mb={4}>From</Text>
<SimpleSelect
value={fromEntity}
onValueChange={setFromEntity}
options={entities.map(e => ({ value: e.id, label: e.name }))}
/>
</div>
<div className="flex-1">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>To</label>
<div>
<Text size="sm" fw={500} mb={4}>To</Text>
<SimpleSelect
value={toEntity}
onValueChange={setToEntity}
options={entities.map(e => ({ value: e.id, label: e.name }))}
/>
</div>
</div>
</Group>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Description</label>
<input value={description} onChange={e => setDescription(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
</div>
<TextInput
label="Description"
value={description}
onChange={e => setDescription(e.currentTarget.value)}
size="sm"
/>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Weight (0.0 - 1.0)</label>
<input value={weight} onChange={e => setWeight(e.target.value)} type="number" step="0.1" min="0" max="1" className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
</div>
<NumberInput
label="Weight (0.0 - 1.0)"
value={weight}
onChange={setWeight}
step={0.1}
min={0}
max={1}
decimalScale={2}
size="sm"
/>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Notes</label>
<textarea value={notes} onChange={e => setNotes(e.target.value)} rows={2} className="w-full px-3 py-1.5 rounded text-sm resize-none" style={inputStyle} />
</div>
</div>
<Textarea
label="Notes"
value={notes}
onChange={e => setNotes(e.currentTarget.value)}
rows={2}
size="sm"
/>
</Stack>
<div className="flex justify-end gap-2 mt-4">
<button onClick={onClose} className="px-3 py-1.5 rounded text-sm" style={{ background: 'var(--secondary)', color: 'var(--secondary-foreground)' }}>Cancel</button>
<button
onClick={handleSubmit}
disabled={!name || fromEntity === toEntity}
className="px-3 py-1.5 rounded text-sm font-medium disabled:opacity-40"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
>
Create
</button>
</div>
</div>
</div>
<DialogFooter>
<Group justify="flex-end" gap="sm" mt="md">
<Button variant="secondary" onClick={onClose}>Cancel</Button>
<Button onClick={handleSubmit} disabled={!name || fromEntity === toEntity}>
Create
</Button>
</Group>
</DialogFooter>
</DialogContent>
</Dialog>
)
}