266 lines
9.3 KiB
Go
266 lines
9.3 KiB
Go
package productDescriptionService
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.ma-al.com/goc_daniel/b2b/app/db"
|
|
"git.ma-al.com/goc_daniel/b2b/app/model"
|
|
"git.ma-al.com/goc_daniel/b2b/app/service/langsService"
|
|
"git.ma-al.com/goc_daniel/b2b/app/utils/responseErrors"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/openai/openai-go/v3"
|
|
"github.com/openai/openai-go/v3/option"
|
|
"github.com/openai/openai-go/v3/responses"
|
|
)
|
|
|
|
type ProductDescriptionService struct {
|
|
db *gorm.DB
|
|
client openai.Client
|
|
}
|
|
|
|
func New() *ProductDescriptionService {
|
|
return &ProductDescriptionService{
|
|
db: db.Get(),
|
|
client: openai.NewClient(option.WithAPIKey("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")),
|
|
}
|
|
}
|
|
|
|
func isValidXHTML(s string) bool {
|
|
decoder := xml.NewDecoder(strings.NewReader(s))
|
|
hasStartTag := false
|
|
|
|
for {
|
|
tok, err := decoder.Token()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return hasStartTag
|
|
}
|
|
return false
|
|
}
|
|
|
|
if _, ok := tok.(xml.StartElement); ok {
|
|
hasStartTag = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// We assume that any user has access to all product descriptions
|
|
func (s *ProductDescriptionService) GetProductDescription(userID uint, productID uint, productShopID uint, productLangID uint) (*model.ProductDescription, error) {
|
|
var ProductDescription model.ProductDescription
|
|
|
|
err := s.db.
|
|
Table("ps_product_lang").
|
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
First(&ProductDescription).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
return &ProductDescription, nil
|
|
}
|
|
|
|
// Updates relevant fields with the "updates" map
|
|
func (s *ProductDescriptionService) SaveProductDescription(userID uint, productID uint, productShopID uint, productLangID uint, updates map[string]string) error {
|
|
// only some fields can be affected
|
|
allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"}
|
|
for key := range updates {
|
|
if !slices.Contains(allowedFields, key) {
|
|
return responseErrors.ErrBadField
|
|
}
|
|
}
|
|
|
|
// check that fields description, description_short and usage, if they exist, have a valid html format
|
|
mustBeHTML := []string{"description", "description_short", "usage"}
|
|
for i := 0; i < len(mustBeHTML); i++ {
|
|
if text, exists := updates[mustBeHTML[i]]; exists {
|
|
if !isValidXHTML(text) {
|
|
return responseErrors.ErrInvalidHTML
|
|
}
|
|
}
|
|
}
|
|
|
|
record := model.ProductDescription{
|
|
ProductID: productID,
|
|
ShopID: productShopID,
|
|
LangID: productLangID,
|
|
}
|
|
|
|
err := s.db.
|
|
Table("ps_product_lang").
|
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
FirstOrCreate(&record).Error
|
|
if err != nil {
|
|
return fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
updatesIface := make(map[string]interface{}, len(updates))
|
|
for k, v := range updates {
|
|
updatesIface[k] = v
|
|
}
|
|
|
|
err = s.db.
|
|
Table("ps_product_lang").
|
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productLangID).
|
|
Updates(updatesIface).Error
|
|
if err != nil {
|
|
return fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Updates relevant fields with the "updates" map
|
|
func (s *ProductDescriptionService) TranslateProductDescription(userID uint, productID uint, productShopID uint, productFromLangID uint, productToLangID uint) (*model.ProductDescription, error) {
|
|
var ProductDescription model.ProductDescription
|
|
|
|
err := s.db.
|
|
Table("ps_product_lang").
|
|
Where("id_product = ? AND id_shop = ? AND id_lang = ?", productID, productShopID, productFromLangID).
|
|
First(&ProductDescription).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("database error: %w", err)
|
|
}
|
|
|
|
// we translate all changeable fields, and we keep the exact same HTML structure in relevant fields.
|
|
lang, err := langsService.LangSrv.GetLanguageById(productToLangID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request := "Translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website.\n\n"
|
|
request += "\n"
|
|
request += "<translation_of_product_description>"
|
|
request += ProductDescription.Description
|
|
request += "</translation_of_product_description>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_product_short_description>"
|
|
request += ProductDescription.DescriptionShort
|
|
request += "</translation_of_product_short_description>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_product_meta_description>"
|
|
request += ProductDescription.MetaDescription
|
|
request += "</translation_of_product_meta_description>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_product_meta_title>"
|
|
request += ProductDescription.MetaTitle
|
|
request += "</translation_of_product_meta_title>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_product_name>"
|
|
request += ProductDescription.Name
|
|
request += "</translation_of_product_name>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_display_text_available_now>"
|
|
request += ProductDescription.AvailableNow
|
|
request += "</translation_of_display_text_available_now>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_display_text_available_later>"
|
|
request += ProductDescription.AvailableLater
|
|
request += "</translation_of_display_text_available_later>"
|
|
request += "\n"
|
|
request += "Remember: translate to " + lang.ISOCode + " without changing the html structure. You must only translate text visible on website."
|
|
request += "\n"
|
|
request += "<translation_of_product_usage>"
|
|
request += ProductDescription.Usage
|
|
request += "</translation_of_product_usage>"
|
|
|
|
openai_response, err := s.client.Responses.New(context.Background(), responses.ResponseNewParams{
|
|
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(request)},
|
|
Model: openai.ChatModelGPT4_1Mini,
|
|
// Model: openai.ChatModelGPT4_1Nano,
|
|
})
|
|
if openai_response.Status != "completed" {
|
|
return nil, responseErrors.ErrOpenAIResponseFail
|
|
}
|
|
output := openai_response.OutputText()
|
|
|
|
// for debugging purposes
|
|
// fi, err := os.ReadFile("/home/daniel/coding/work/b2b/app/service/productDescriptionService/test.txt") // just pass the file name
|
|
// output := string(fi)
|
|
|
|
success, match := GetStringInBetween(output, "<translation_of_product_description>", "</translation_of_product_description>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.Description = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_product_short_description>", "</translation_of_product_short_description>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.DescriptionShort = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_product_meta_description>", "</translation_of_product_meta_description>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.MetaDescription = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_product_meta_title>", "</translation_of_product_meta_title>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.MetaTitle = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_product_name>", "</translation_of_product_name>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.Name = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_display_text_available_now>", "</translation_of_display_text_available_now>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.AvailableNow = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_display_text_available_later>", "</translation_of_display_text_available_later>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.AvailableLater = match
|
|
|
|
success, match = GetStringInBetween(output, "<translation_of_product_usage>", "</translation_of_product_usage>")
|
|
if !success {
|
|
return nil, responseErrors.ErrOpenAIBadOutput
|
|
}
|
|
ProductDescription.Usage = match
|
|
|
|
return &ProductDescription, nil
|
|
}
|
|
|
|
// GetStringInBetween returns empty string if no start or end string found
|
|
func GetStringInBetween(str string, start string, end string) (success bool, result string) {
|
|
s := strings.Index(str, start)
|
|
if s == -1 {
|
|
return false, ""
|
|
}
|
|
s += len(start)
|
|
e := strings.Index(str[s:], end)
|
|
if e == -1 {
|
|
return false, ""
|
|
}
|
|
|
|
return true, str[s : s+e]
|
|
}
|