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
+59 -69
View File
@@ -1,11 +1,11 @@
import { useState, useEffect } from 'react'
import { Stack, Group, TextInput, Textarea, Text } from '@mantine/core'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, SimpleSelect, Button } from '@fn_library'
import type { Entity, EntityTypePreset, EntityInput } from '../types'
import { SimpleSelect } from '@fn_library'
import { X } from 'lucide-react'
interface Props {
presets: EntityTypePreset[]
entity: Entity | null // null = create, non-null = edit
entity: Entity | null
onSubmit: (input: EntityInput) => void
onClose: () => void
}
@@ -29,7 +29,6 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
const currentPreset = presets.find(p => p.type_ref === typeRef)
const metadataFields = currentPreset?.metadata_fields ?? []
// When type changes, reset metadata fields to match new type
useEffect(() => {
if (!entity) {
const m: Record<string, string> = {}
@@ -44,7 +43,6 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
const cleanMeta: Record<string, unknown> = {}
for (const [k, v] of Object.entries(metadata)) {
if (v.trim()) {
// Try parsing as number
const num = Number(v)
if (!isNaN(num) && v.trim() !== '') {
cleanMeta[k] = num
@@ -68,28 +66,23 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
})
}
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-[520px] max-h-[85vh] overflow-y-auto 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">{entity ? 'Edit Entity' : 'New Entity'}</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>{entity ? 'Edit Entity' : 'New Entity'}</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Name</label>
<input value={name} onChange={e => setName(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
</div>
<Stack gap="sm">
<TextInput
label="Name"
value={name}
onChange={e => setName(e.currentTarget.value)}
size="sm"
/>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Type</label>
<Text size="sm" fw={500} mb={4}>Type</Text>
<SimpleSelect
value={typeRef}
onValueChange={setTypeRef}
@@ -97,64 +90,61 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
/>
</div>
<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)' }}>Tags (comma separated)</label>
<input value={tagsStr} onChange={e => setTagsStr(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} placeholder="osint, high-risk" />
</div>
<TextInput
label="Tags (comma separated)"
value={tagsStr}
onChange={e => setTagsStr(e.currentTarget.value)}
placeholder="osint, high-risk"
size="sm"
/>
{metadataFields.length > 0 && (
<div>
<label className="block text-xs mb-1 font-semibold" style={{ color: 'var(--muted-foreground)' }}>
<Text size="xs" fw={600} c="dimmed" mb="xs">
Metadata ({currentPreset?.label})
</label>
<div className="space-y-2">
</Text>
<Stack gap="xs">
{metadataFields.map(field => (
<div key={field} className="flex items-center gap-2">
<span className="text-xs w-28 text-right" style={{ color: 'var(--muted-foreground)' }}>{field}</span>
<input
<Group key={field} gap="sm" align="center">
<Text size="xs" c="dimmed" w={112} ta="right">{field}</Text>
<TextInput
value={metadata[field] ?? ''}
onChange={e => setMetadata(prev => ({ ...prev, [field]: e.target.value }))}
className="flex-1 px-2 py-1 rounded text-sm"
style={inputStyle}
onChange={e => setMetadata(prev => ({ ...prev, [field]: e.currentTarget.value }))}
size="xs"
flex={1}
/>
</div>
</Group>
))}
</div>
</Stack>
</div>
)}
<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={3}
className="w-full px-3 py-1.5 rounded text-sm resize-none"
style={inputStyle}
placeholder="Operational notes..."
/>
</div>
</div>
<Textarea
label="Notes"
value={notes}
onChange={e => setNotes(e.currentTarget.value)}
rows={3}
placeholder="Operational notes..."
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.trim()}
className="px-3 py-1.5 rounded text-sm font-medium disabled:opacity-40"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
>
{entity ? 'Update' : '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.trim()}>
{entity ? 'Update' : 'Create'}
</Button>
</Group>
</DialogFooter>
</DialogContent>
</Dialog>
)
}