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
+198 -7
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"log"
"os"
@@ -580,20 +581,210 @@ func (a *App) GetRelationPresets() []string {
return relationPresets
}
// --- Enrichers ---
func (a *App) GetEnrichers() []EnricherDef {
log.Printf("[GetEnrichers] returning %d enrichers", len(enricherRegistry))
return enricherRegistry
}
func (a *App) GetEnrichersForEntity(entityID string) ([]EnricherDef, error) {
a.mu.RLock()
defer a.mu.RUnlock()
if a.db == nil {
return nil, fmt.Errorf("no project selected")
}
entity, err := a.db.GetEntity(entityID)
if err != nil {
return nil, err
}
if entity == nil {
return nil, fmt.Errorf("entity %s not found", entityID)
}
result := enrichersForType(entity.TypeRef)
log.Printf("[GetEnrichersForEntity] entityID=%s typeRef=%s -> %d enrichers", entityID, entity.TypeRef, len(result))
return result, nil
}
func (a *App) RunEnricher(enricherID, entityID string) (GraphData, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.db == nil {
return GraphData{}, fmt.Errorf("no project selected")
}
log.Printf("[RunEnricher] enricherID=%s entityID=%s", enricherID, entityID)
enricher := findEnricher(enricherID)
if enricher == nil {
return GraphData{}, fmt.Errorf("enricher %s not found", enricherID)
}
entity, err := a.db.GetEntity(entityID)
if err != nil {
return GraphData{}, err
}
if entity == nil {
return GraphData{}, fmt.Errorf("entity %s not found", entityID)
}
// Serialize entity to JSON for the Python script
entityJSON, err := json.Marshal(map[string]any{
"id": entity.ID,
"name": entity.Name,
"type_ref": entity.TypeRef,
"metadata": entity.Metadata,
})
if err != nil {
return GraphData{}, fmt.Errorf("marshaling entity: %w", err)
}
// Run enricher
enrichersDir := filepath.Join(filepath.Dir(os.Args[0]), "enrichers")
// Fallback: try relative to working directory
if _, err := os.Stat(enrichersDir); err != nil {
enrichersDir = "enrichers"
}
// Fallback: try relative to project dir
if _, err := os.Stat(filepath.Join(enrichersDir, enricher.Script)); err != nil {
// Try from the app source directory
if exePath, err2 := os.Executable(); err2 == nil {
enrichersDir = filepath.Join(filepath.Dir(exePath), "enrichers")
}
}
log.Printf("[RunEnricher] executing %s in %s", enricher.Script, enrichersDir)
result, err := runEnricherScript(a.registryRoot, enrichersDir, enricher.Script, entityJSON)
if err != nil {
log.Printf("[RunEnricher] ERROR: %v", err)
return GraphData{}, err
}
log.Printf("[RunEnricher] result: %d entities, %d relations", len(result.Entities), len(result.Relations))
// Insert results into operations.db
if err := a.insertEnricherResults(result, entityID); err != nil {
log.Printf("[RunEnricher] ERROR inserting results: %v", err)
return GraphData{}, err
}
// Return full graph
data, err := buildGraphData(a.db)
if err != nil {
return GraphData{}, err
}
log.Printf("[RunEnricher] OK: graph now has %d nodes, %d edges", len(data.Nodes), len(data.Edges))
return data, nil
}
// --- Ingest ---
func (a *App) IngestURL(url string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.db == nil {
return "", fmt.Errorf("no project selected")
}
url = strings.TrimSpace(url)
if url == "" {
return "", fmt.Errorf("URL cannot be empty")
}
log.Printf("[IngestURL] url=%s", url)
id := makeEntityID(url, "url")
now := time.Now()
e := &ops.Entity{
ID: id,
Name: url,
TypeRef: "url",
Status: ops.StatusActive,
Description: "Ingested URL",
Domain: "fuzzygraph",
Tags: []string{"ingested"},
Source: "fuzzygraph",
Metadata: map[string]any{"url": url},
CreatedAt: now,
UpdatedAt: now,
}
if a.registryDB != nil {
if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil {
if err2 := a.db.InsertEntity(e); err2 != nil {
return "", err2
}
}
} else {
if err := a.db.InsertEntity(e); err != nil {
return "", err
}
}
log.Printf("[IngestURL] OK: %s", id)
return id, nil
}
func (a *App) IngestFile(filePath string) (string, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.db == nil {
return "", fmt.Errorf("no project selected")
}
filePath = strings.TrimSpace(filePath)
if filePath == "" {
return "", fmt.Errorf("file path cannot be empty")
}
log.Printf("[IngestFile] path=%s", filePath)
name := filepath.Base(filePath)
ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
id := makeEntityID(name, "document")
now := time.Now()
e := &ops.Entity{
ID: id,
Name: name,
TypeRef: "document",
Status: ops.StatusActive,
Description: fmt.Sprintf("Ingested document (%s)", ext),
Domain: "fuzzygraph",
Tags: []string{"ingested"},
Source: "fuzzygraph",
Metadata: map[string]any{"file_path": filePath, "format": ext},
CreatedAt: now,
UpdatedAt: now,
}
if a.registryDB != nil {
if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil {
if err2 := a.db.InsertEntity(e); err2 != nil {
return "", err2
}
}
} else {
if err := a.db.InsertEntity(e); err != nil {
return "", err
}
}
log.Printf("[IngestFile] OK: %s", id)
return id, nil
}
// --- Helpers ---
func makeEntityID(name, typeRef string) string {
clean := strings.ToLower(strings.TrimSpace(name))
clean = strings.ReplaceAll(clean, " ", "_")
clean = strings.ReplaceAll(clean, "-", "_")
parts := strings.Split(typeRef, "_")
shortType := typeRef
if len(parts) >= 2 {
shortType = parts[1]
// Truncate long names (URLs etc.)
if len(clean) > 60 {
clean = clean[:60]
}
return fmt.Sprintf("%s_%s", clean, shortType)
return fmt.Sprintf("%s_%s", clean, typeRef)
}
func generateID() string {