feat(umind): ver qué se leyó de cada fuente, fragmento por fragmento
Una fuente crawleada decía "9 fragmentos" y nada más. Las notas y archivos al menos tienen Editar, que muestra su texto; un sitio no tiene nada — era una caja negra, sin forma de saber si el crawler leyó los precios o el pie de página. Y cuando el agente contesta mal, la primera pregunta es justamente qué tiene cargado de verdad. Cada fuente gana un "Ver" que abre sus fragmentos tal como quedaron indexados, numerados y en orden, con la advertencia que importa: esto es exactamente lo que el asistente puede consultar — si acá falta algo, eso mismo le va a faltar en las respuestas. El botón se apaga cuando la fuente no tiene fragmentos todavía (procesando o con error): abrir un visor vacío no informa nada. El endpoint pasa por accesoDocumento como todos los sub-recursos, y devuelve solo el contenido — el embedding no viaja: son miles de números que no le sirven a nadie en pantalla. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
be01d95631
commit
ffa24a068f
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dev",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"--prefix",
|
||||
"orchestrator",
|
||||
"run",
|
||||
"dev",
|
||||
"--",
|
||||
"--port",
|
||||
"3000",
|
||||
"--strictPort"
|
||||
],
|
||||
"port": 3000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -120,6 +120,28 @@ async function subirArchivo() {
|
||||
|
||||
// Editar una fuente escrita a mano. Las plantillas de rubro dejan las notas
|
||||
// con valores entre corchetes para reemplazar — sin esto, no había con qué.
|
||||
// Ver qué se leyó de una fuente, fragmento por fragmento. Para un sitio
|
||||
// crawleado es la única ventana: no tiene Editar (se rehace crawleando) y
|
||||
// hasta ahora "9 fragmentos" era una caja negra.
|
||||
const viendoDoc = ref(null)
|
||||
const fragmentos = ref([])
|
||||
const cargandoFragmentos = ref(false)
|
||||
|
||||
async function verFragmentos(d) {
|
||||
viendoDoc.value = d
|
||||
fragmentos.value = []
|
||||
cargandoFragmentos.value = true
|
||||
try {
|
||||
const r = await api.get(apiUmind(`/umind/documentos/${d.ID}/fragmentos`))
|
||||
fragmentos.value = r.items || []
|
||||
} catch (e) {
|
||||
error.value = e.message
|
||||
viendoDoc.value = null
|
||||
} finally {
|
||||
cargandoFragmentos.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editandoDoc = ref(null)
|
||||
const docForm = ref({ titulo: '', contenido: '' })
|
||||
const guardandoDoc = ref(false)
|
||||
@@ -473,6 +495,13 @@ watch(
|
||||
:title="d.auto_actualizar ? 'Dejar de releer sola' : 'Releer el sitio cada semana'"
|
||||
@click="alternarAuto(d)"
|
||||
><UiIcono :nombre="d.auto_actualizar ? 'auto' : 'refrescar'" :tam="13" class="mr-1" />{{ d.auto_actualizar ? 'auto' : 'manual' }}</button>
|
||||
<button
|
||||
class="text-sm text-tenue hover:text-texto"
|
||||
:disabled="!d.total_chunks"
|
||||
:class="!d.total_chunks ? 'opacity-40 cursor-default' : ''"
|
||||
title="Ver qué se leyó de esta fuente"
|
||||
@click="verFragmentos(d)"
|
||||
>Ver</button>
|
||||
<!-- Solo lo que tiene texto propio guardado: una URL se rehace
|
||||
crawleando, editarla a mano se perdería en la próxima pasada. -->
|
||||
<button
|
||||
@@ -501,6 +530,41 @@ watch(
|
||||
<TabChat :agente-id="agenteIdNum" />
|
||||
</div>
|
||||
|
||||
<!-- Ver los fragmentos de una fuente, solo lectura -->
|
||||
<div
|
||||
v-if="viendoDoc"
|
||||
class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50"
|
||||
@click.self="viendoDoc = null"
|
||||
>
|
||||
<div class="card w-full max-w-2xl p-6 max-h-[calc(100vh-2rem)] overflow-y-auto">
|
||||
<div class="flex items-baseline justify-between gap-3 mb-1">
|
||||
<h2 class="font-semibold text-texto truncate">{{ viendoDoc.origen }}</h2>
|
||||
<span class="text-xs text-tenue shrink-0 tabular-nums">
|
||||
{{ fragmentos.length }} {{ fragmentos.length === 1 ? 'fragmento' : 'fragmentos' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-tenue mb-4">
|
||||
Esto es exactamente lo que el asistente puede consultar de esta fuente.
|
||||
Si acá falta algo o se leyó mal, eso mismo le va a faltar en las respuestas.
|
||||
</p>
|
||||
|
||||
<p v-if="cargandoFragmentos" class="text-sm text-tenue">Cargando…</p>
|
||||
<div v-else class="space-y-2">
|
||||
<div
|
||||
v-for="(f, i) in fragmentos"
|
||||
:key="f.ID"
|
||||
class="bg-elevado border border-borde rounded-lg p-3 text-sm text-texto whitespace-pre-wrap"
|
||||
>
|
||||
<span class="text-[10px] text-tenue tabular-nums block mb-1">#{{ i + 1 }}</span>{{ f.contenido }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4">
|
||||
<button class="btn-ghost" @click="viendoDoc = null">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editar una nota o el texto extraído de un archivo -->
|
||||
<div
|
||||
v-if="editandoDoc"
|
||||
|
||||
@@ -287,6 +287,19 @@ func CreateUmindChunks(chunks []UmindChunk) error {
|
||||
|
||||
// GetUmindChunksByAgente retorna todos los chunks del agente, para la
|
||||
// búsqueda por similitud en memoria.
|
||||
// GetUmindChunksByDocumento devuelve los fragmentos de UNA fuente, en el orden
|
||||
// en que se guardaron. Es lo que deja ver qué se leyó de verdad de un sitio o
|
||||
// un archivo: sin esto, una URL crawleada era una caja negra — "9 fragmentos"
|
||||
// y ninguna forma de saber si el crawler leyó los precios o el pie de página.
|
||||
func GetUmindChunksByDocumento(documentoID uint) ([]UmindChunk, error) {
|
||||
var items []UmindChunk
|
||||
err := app.Http.Database.DB.
|
||||
Where("documento_id = ?", documentoID).
|
||||
Order("id ASC").
|
||||
Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func GetUmindChunksByAgente(agenteID uint) ([]UmindChunk, error) {
|
||||
var items []UmindChunk
|
||||
err := app.Http.Database.DB.Where("agente_id = ?", agenteID).Find(&items).Error
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' rx='22' fill='%238eb02f'/%3E%3Cpath d='M32,42 V58 A14,14 0 0 0 60,58 V42' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round' stroke-linejoin='round'/%3E%3Cpath d='M60,58 V64' fill='none' stroke='%23fff' stroke-width='10' stroke-linecap='round'/%3E%3Ccircle cx='60' cy='28' r='7' fill='%23fff'/%3E%3C/svg%3E" />
|
||||
<title>uMind Studio</title>
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-NXjEScsg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CZn9dJyg.css">
|
||||
<script type="module" crossorigin src="/orchestrator/assets/index-kEapBzai.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/orchestrator/assets/index-CeuZ8PUd.css">
|
||||
</head>
|
||||
<!-- Sin clase de fondo: el color lo pone body en style.css desde los tokens,
|
||||
que son los que cambian con el tema. Una utilidad acá le ganaba a la
|
||||
|
||||
@@ -1092,3 +1092,26 @@ func DuplicarUmindAgenteHandler(c *fiber.Ctx) error {
|
||||
"aviso": "El conocimiento se está copiando en segundo plano. Los canales y las claves de las herramientas no se copian: hay que configurarlos en la copia.",
|
||||
})
|
||||
}
|
||||
|
||||
// GetFragmentosDocumentoHandler muestra lo que se leyó de una fuente, fragmento
|
||||
// por fragmento. Es la única ventana a lo que el agente tiene de verdad: el
|
||||
// contenido nunca se muestra entero en ningún otro lado.
|
||||
// GET /umind/documentos/:id/fragmentos
|
||||
func GetFragmentosDocumentoHandler(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"})
|
||||
}
|
||||
if err := accesoDocumento(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
chunks, err := models.GetUmindChunksByDocumento(uint(id))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
out := make([]fiber.Map, len(chunks))
|
||||
for i, ch := range chunks {
|
||||
out[i] = fiber.Map{"ID": ch.ID, "contenido": ch.Contenido}
|
||||
}
|
||||
return c.JSON(fiber.Map{"items": out})
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ func RegistrarRutasUmind(g fiber.Router, scope fiber.Handler, escritura fiber.Ha
|
||||
g.Post("/umind/documentos", w(controllers.CreateUmindDocumentoHandler)...)
|
||||
g.Post("/umind/documentos/texto", w(controllers.CreateUmindTextoHandler)...)
|
||||
g.Post("/umind/documentos/archivo", w(controllers.CreateUmindArchivoHandler)...)
|
||||
g.Get("/umind/documentos/:id/fragmentos", r(controllers.GetFragmentosDocumentoHandler)...)
|
||||
g.Put("/umind/documentos/:id", w(controllers.ActualizarUmindDocumentoHandler)...)
|
||||
g.Post("/umind/documentos/:id/reprocesar", w(controllers.ReprocesarUmindDocumentoHandler)...)
|
||||
g.Delete("/umind/documentos/:id", w(controllers.DeleteUmindDocumentoHandler)...)
|
||||
|
||||
Reference in New Issue
Block a user