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, "", "\n") s = strings.ReplaceAll(s, "", "\n") s = strings.ReplaceAll(s, "", "\n") s = strings.ReplaceAll(s, "", "\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