Hasta ahora crear una plantilla era escribir HTML con variables de Go a mano. Ahora se sube el documento que ya existe (.docx, .html, .txt o una foto del papel), se lee —docx con stdlib, imágenes por OCR— y la IA lo devuelve armado como plantilla con las variables del generador puestas donde iban los datos. No se guarda solo: el HTML cae en el editor para revisarlo, y si la IA devolvió sintaxis de plantilla inválida se avisa antes de guardar. PDF queda afuera por ahora; el mensaje de error dice qué hacer en su lugar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
5.1 KiB
Go
145 lines
5.1 KiB
Go
package services
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// variablesPorTipo documenta, para la IA, qué campos recibe cada plantilla al
|
|
// renderizarse. Sale de DatosBaseDocumento + lo que arma cada generador
|
|
// (ver CrearCotizacion, contrato_documento_service, cuenta_cobro_documento_service).
|
|
var variablesPorTipo = map[string]string{
|
|
"cotizacion": `{{.Cliente.Nombre}}, {{.Cliente.Nit}}, {{.Cliente.Email}}, {{.Cliente.Telefono}},
|
|
{{.Alcance}}, {{.TipoProyecto}}, {{.Total}},
|
|
{{range .Items}} … {{.Descripcion}} {{.Cantidad}} {{.Unidad}} {{.ValorUnitario}} … {{end}}`,
|
|
"contrato": `{{.Cliente.Nombre}}, {{.Cliente.Nit}}, {{.Alcance}}, {{.Total}}, {{.Servicio}}, {{.Periodicidad}}`,
|
|
"acta": `{{.Cliente.Nombre}}, {{.Proyecto}}, {{.Alcance}}, {{.Entregables}}`,
|
|
"cuenta_cobro": `{{.Cliente.Nombre}}, {{.Cliente.Nit}}, {{.Concepto}}, {{.Total}}, {{.Numero}}`,
|
|
}
|
|
|
|
const variablesComunes = `{{.Fecha}}, {{.EmpresaNombre}}, {{.EmpresaWeb}}`
|
|
|
|
// ExtraerTextoDePlantilla saca el texto de un archivo subido para usarlo como
|
|
// referencia. Los formatos de texto se leen directo; el .docx es un zip con XML
|
|
// adentro (stdlib alcanza) y las imágenes pasan por OCR.
|
|
func ExtraerTextoDePlantilla(nombreArchivo string, datos []byte) (string, error) {
|
|
ext := strings.ToLower(filepath.Ext(nombreArchivo))
|
|
switch ext {
|
|
case ".html", ".htm", ".txt", ".md":
|
|
return string(datos), nil
|
|
case ".docx":
|
|
return textoDeDocx(datos)
|
|
case ".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff":
|
|
mime := "image/png"
|
|
if ext == ".jpg" || ext == ".jpeg" {
|
|
mime = "image/jpeg"
|
|
} else if ext != ".png" {
|
|
mime = "image/" + strings.TrimPrefix(ext, ".")
|
|
}
|
|
// agenteID 0: es una acción del staff, no se le cobra a ningún cliente.
|
|
return ExtraerTextoOCR(0, datos, mime)
|
|
case ".pdf":
|
|
return "", fmt.Errorf("el PDF todavía no se puede leer acá; exportalo a .docx o subí una captura de pantalla del documento")
|
|
default:
|
|
return "", fmt.Errorf("formato %s no soportado: subí .docx, .html, .txt o una imagen del documento", ext)
|
|
}
|
|
}
|
|
|
|
var etiquetaXML = regexp.MustCompile(`<[^>]+>`)
|
|
|
|
// textoDeDocx lee word/document.xml del .docx y lo aplana a texto. No pretende
|
|
// conservar el formato: la IA solo necesita el contenido y el orden.
|
|
func textoDeDocx(datos []byte) (string, error) {
|
|
zr, err := zip.NewReader(bytes.NewReader(datos), int64(len(datos)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("el .docx no se pudo abrir: %w", err)
|
|
}
|
|
for _, f := range zr.File {
|
|
if f.Name != "word/document.xml" {
|
|
continue
|
|
}
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer rc.Close()
|
|
xmlBytes, err := io.ReadAll(io.LimitReader(rc, 8<<20))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
s := string(xmlBytes)
|
|
// Un párrafo y un salto de línea explícito valen como salto de línea;
|
|
// el resto de las etiquetas se descarta.
|
|
s = strings.ReplaceAll(s, "</w:p>", "\n")
|
|
s = strings.ReplaceAll(s, "<w:br/>", "\n")
|
|
s = strings.ReplaceAll(s, "</w:tr>", "\n")
|
|
s = strings.ReplaceAll(s, "</w:tc>", "\t")
|
|
s = etiquetaXML.ReplaceAllString(s, "")
|
|
s = strings.NewReplacer("&", "&", "<", "<", ">", ">", """, `"`, "'", "'").Replace(s)
|
|
return strings.TrimSpace(s), nil
|
|
}
|
|
return "", fmt.Errorf("el archivo no parece un .docx (no tiene word/document.xml)")
|
|
}
|
|
|
|
// ConvertirEnPlantilla le pide a la IA que rearme el documento como plantilla
|
|
// HTML con las variables Go que usa el generador. Lo que devuelve va al editor
|
|
// para que el admin lo revise antes de guardar: no se guarda solo.
|
|
func ConvertirEnPlantilla(tipo, textoDocumento string) (string, error) {
|
|
if strings.TrimSpace(textoDocumento) == "" {
|
|
return "", fmt.Errorf("no se pudo leer texto del archivo")
|
|
}
|
|
if len(textoDocumento) > 20000 {
|
|
textoDocumento = textoDocumento[:20000]
|
|
}
|
|
vars := variablesPorTipo[tipo]
|
|
if vars == "" {
|
|
vars = variablesComunes
|
|
}
|
|
|
|
sistema := `Sos un asistente que convierte documentos en plantillas HTML para Go text/template.
|
|
Reglas:
|
|
- Devolvé SOLO el HTML de la plantilla, sin explicaciones y sin bloques de código markdown.
|
|
- Reemplazá los datos concretos del documento (nombres, NITs, fechas, montos, ítems) por las variables de la lista. Lo que sea texto fijo del formato se deja tal cual.
|
|
- Si el documento tiene una tabla de ítems, usá {{range .Items}}…{{end}} para las filas.
|
|
- Usá estilos inline (style="…"), sin CSS externo ni <script>: el HTML se convierte a PDF.
|
|
- No inventes variables que no estén en la lista.`
|
|
|
|
usuario := fmt.Sprintf(`Tipo de documento: %s
|
|
|
|
Variables disponibles:
|
|
%s
|
|
%s
|
|
|
|
Documento de referencia:
|
|
---
|
|
%s
|
|
---`, tipo, variablesComunes, vars, textoDocumento)
|
|
|
|
salida, err := CompletarTextoIA("ia", sistema, usuario)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return limpiarCercaDeCodigo(salida), nil
|
|
}
|
|
|
|
// limpiarCercaDeCodigo saca el ```html … ``` que los modelos agregan aunque se
|
|
// les pida que no lo hagan.
|
|
func limpiarCercaDeCodigo(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if !strings.HasPrefix(s, "```") {
|
|
return s
|
|
}
|
|
if i := strings.Index(s, "\n"); i >= 0 {
|
|
s = s[i+1:]
|
|
}
|
|
if i := strings.LastIndex(s, "```"); i >= 0 {
|
|
s = s[:i]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|