fix
This commit is contained in:
@@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "config: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||||
|
MaxConns: int32(cfg.DBMaxConns), MinConns: int32(cfg.DBMinConns),
|
||||||
|
MaxConnLifetime: cfg.DBMaxConnLifetime, MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
|
||||||
|
MaxConnIdleTime: cfg.DBMaxConnIdleTime, HealthCheckPeriod: cfg.DBHealthCheckPeriod,
|
||||||
|
StatementTimeout: cfg.DBStatementTimeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "db: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
platEnv := platformsettings.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
|
||||||
|
}
|
||||||
|
plat := platformsettings.NewService(pool, platEnv)
|
||||||
|
ai := aiprovider.NewService(pool, aiprovider.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
|
||||||
|
})
|
||||||
|
ai.Platform = plat
|
||||||
|
|
||||||
|
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790")
|
||||||
|
roleCfg, err := plat.ResolveAIConfig(ctx, platformsettings.AIRoleProcessing)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ResolveAIConfig: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
completer, mode, byok, err := ai.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "ResolveCompleter: %v\n", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
client, ok := completer.(*processing.OpenAIClient)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "not OpenAIClient: %T\n", completer)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(client.BaseURL), "/")
|
||||||
|
model := strings.TrimSpace(client.Model)
|
||||||
|
if processing.IsMockOrLoopbackBaseURL(base) {
|
||||||
|
fmt.Println("FAIL mock/loopback")
|
||||||
|
os.Exit(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("resolved source=%s provider=%s base=%s model=%s mode=%s byok=%v key_len=%d app_max_tokens_enhance=%d retry=%d http_timeout=240s\n",
|
||||||
|
roleCfg.Source, roleCfg.Provider, base, model, mode, byok, len(client.APIKey),
|
||||||
|
processing.MaxTokensEnhance, processing.MaxTokensEnhanceRetry)
|
||||||
|
|
||||||
|
// Optional: adjust model id if gateway wants green/ prefix (prior resolve used both).
|
||||||
|
models := []string{model}
|
||||||
|
if !strings.HasPrefix(model, "green/") {
|
||||||
|
models = append(models, "green/"+model)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := "tiny"
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
size = strings.ToLower(strings.TrimSpace(os.Args[1]))
|
||||||
|
}
|
||||||
|
maxTok := 256
|
||||||
|
prompt := "Reply with exactly: OK"
|
||||||
|
timeout := 60 * time.Second
|
||||||
|
switch size {
|
||||||
|
case "tiny":
|
||||||
|
maxTok = 256
|
||||||
|
prompt = "Reply with exactly one word: OK"
|
||||||
|
timeout = 60 * time.Second
|
||||||
|
case "medium":
|
||||||
|
maxTok = 3072
|
||||||
|
prompt = "Write a short JSON object with keys title and description for product Sony WH-1000XM5 headphones. description must be 2 short HTML paragraphs. Keep under 400 words."
|
||||||
|
timeout = 90 * time.Second
|
||||||
|
case "large":
|
||||||
|
maxTok = 16384
|
||||||
|
prompt = "Write a long JSON object with keys title, description, attributes. description must be multi-section Slovenian HTML with several h2+p and a ul list for Sony WH-1000XM5. Be thorough."
|
||||||
|
timeout = 120 * time.Second
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "usage: %s [tiny|medium|large]\n", os.Args[0])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
useModel := models[0]
|
||||||
|
if len(os.Args) > 2 && strings.TrimSpace(os.Args[2]) != "" {
|
||||||
|
useModel = strings.TrimSpace(os.Args[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyObj := map[string]any{
|
||||||
|
"model": useModel,
|
||||||
|
"temperature": 0.2,
|
||||||
|
"max_tokens": maxTok,
|
||||||
|
"messages": []map[string]string{
|
||||||
|
{"role": "system", "content": "You are a concise assistant."},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
raw, _ := json.Marshal(bodyObj)
|
||||||
|
url := base + "/chat/completions"
|
||||||
|
fmt.Printf("probe size=%s model=%s max_tokens=%d timeout=%s url=%s prompt_len=%d\n",
|
||||||
|
size, useModel, maxTok, timeout, url, len(prompt))
|
||||||
|
|
||||||
|
reqCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, url, bytes.NewReader(raw))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "new request: %v\n", err)
|
||||||
|
os.Exit(4)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.APIKey)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Separate dial vs header wait so we can tell "hung with no headers" from slow body.
|
||||||
|
headerTO := 25 * time.Second
|
||||||
|
if timeout < headerTO {
|
||||||
|
headerTO = timeout
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ResponseHeaderTimeout: headerTO,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
elapsed := time.Since(start).Round(time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("RESULT ok=false elapsed=%s header_timeout=%s err=%v\n", elapsed, headerTO, err)
|
||||||
|
os.Exit(5)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
|
snip := strings.TrimSpace(string(b))
|
||||||
|
if len(snip) > 500 {
|
||||||
|
snip = snip[:500] + "…"
|
||||||
|
}
|
||||||
|
fmt.Printf("RESULT ok=%t status=%d elapsed=%s body_len=%d snippet=%s\n",
|
||||||
|
resp.StatusCode >= 200 && resp.StatusCode < 300, resp.StatusCode, elapsed, len(b), snip)
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
os.Exit(6)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,953 @@
|
|||||||
|
// Temporary local full verification: Sync A1 + live LLM ProcessJob.
|
||||||
|
// Do not commit.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out := os.Getenv("PROBE_OUT_DIR")
|
||||||
|
if out == "" {
|
||||||
|
out = `F:\laragon\www\_MY\descrybe-v2\.codehelper\_final_verify_20260817`
|
||||||
|
}
|
||||||
|
_ = os.MkdirAll(out, 0o755)
|
||||||
|
|
||||||
|
logPath := filepath.Join(out, "inproc_run.log")
|
||||||
|
logFile, err := os.Create(logPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("log file: %v", err)
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logredact.Writer(io.MultiWriter(os.Stderr, logFile)))
|
||||||
|
|
||||||
|
// Repo seed only — no Downloads / admin upload path.
|
||||||
|
repoRoot := `F:\laragon\www\_MY\descrybe-v2`
|
||||||
|
wpPath := filepath.Join(repoRoot, "scripts", "seed", "wp_product_categories.sql")
|
||||||
|
wpBytes, err := os.ReadFile(wpPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("read repo seed wp categories: %v", err)
|
||||||
|
}
|
||||||
|
_ = os.Setenv("SEED_A1_WP_CATEGORIES", wpPath)
|
||||||
|
|
||||||
|
// Prefer issue EANs + mapped-category mix (TV, monitor, headphones, appliance).
|
||||||
|
// 4 mapped-category products (LIVE A1; ~2–4 min/item).
|
||||||
|
eans := []string{
|
||||||
|
"4548736132597", // Slušalke
|
||||||
|
"195348253666", // Gaming monitorji
|
||||||
|
"8806097118565", // Televizorji
|
||||||
|
"3838782459856", // Pomivalni stroji
|
||||||
|
}
|
||||||
|
issueEAN := "" // skip uncategorized issue EAN this pass
|
||||||
|
|
||||||
|
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790") // A1 Slovenija
|
||||||
|
demoID := uuid.MustParse("2b3159b0-fc08-415b-b248-35ed02a6baab") // Platform Demo
|
||||||
|
userID := uuid.MustParse("6bf00877-a693-4d77-b28e-8c8292adac98") // a1-primary@descrybe.local
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("config: %v", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||||
|
MaxConns: int32(cfg.DBMaxConns),
|
||||||
|
MinConns: int32(cfg.DBMinConns),
|
||||||
|
MaxConnLifetime: cfg.DBMaxConnLifetime,
|
||||||
|
MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
|
||||||
|
MaxConnIdleTime: cfg.DBMaxConnIdleTime,
|
||||||
|
HealthCheckPeriod: cfg.DBHealthCheckPeriod,
|
||||||
|
StatementTimeout: cfg.DBStatementTimeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("db: %v", err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
syncReport := map[string]any{
|
||||||
|
"wp_path": wpPath,
|
||||||
|
"wp_bytes": len(wpBytes),
|
||||||
|
"companies": []any{},
|
||||||
|
"generated": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, co := range []struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
Name string
|
||||||
|
}{
|
||||||
|
{companyID, "A1 Slovenija"},
|
||||||
|
{demoID, "Platform Demo"},
|
||||||
|
} {
|
||||||
|
var legacy string
|
||||||
|
_ = pool.QueryRow(ctx, `SELECT COALESCE(legacy_company_id,'') FROM companies WHERE id=$1`, co.ID).Scan(&legacy)
|
||||||
|
a1Cohort := billing.IsA1CohortCompany(legacy, co.Name)
|
||||||
|
res, serr := processing.SyncCompanyA1(ctx, pool, co.ID, co.Name, a1Cohort, processing.SyncCompanyA1Opts{
|
||||||
|
MySQLDumpPath: "", // unused — repo seed only (no Downloads dump)
|
||||||
|
SkipDumpBackfill: true, // do not auto-detect ~/Downloads MySQL dump
|
||||||
|
FixCompanyCatalogOpts: processing.FixCompanyCatalogOpts{
|
||||||
|
BackfillCategories: true,
|
||||||
|
ReprocessSampleLimit: 8,
|
||||||
|
WPCategoriesSQL: wpBytes, // scripts/seed/wp_product_categories.sql
|
||||||
|
AIPrompts: aiprompts.NewService(pool),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
entry := map[string]any{
|
||||||
|
"company_id": co.ID.String(),
|
||||||
|
"company_name": co.Name,
|
||||||
|
"a1_cohort": a1Cohort,
|
||||||
|
"error": nil,
|
||||||
|
"result": res,
|
||||||
|
}
|
||||||
|
if serr != nil {
|
||||||
|
entry["error"] = serr.Error()
|
||||||
|
log.Printf("SyncCompanyA1 %s: %v", co.Name, serr)
|
||||||
|
} else {
|
||||||
|
log.Printf("SyncCompanyA1 %s dump=%s prompts_updated=%d already_ok=%d wp_entries=%d weak_hashes=%d mapped_with=%d",
|
||||||
|
co.Name, res.DumpStatus, res.CategoryPromptsUpdated, res.CategoryPromptsAlreadyOK,
|
||||||
|
res.WPCategoriesEntries, res.WeakHashesCleared, res.MappedWithCategory)
|
||||||
|
}
|
||||||
|
syncReport["companies"] = append(syncReport["companies"].([]any), entry)
|
||||||
|
}
|
||||||
|
writeJSON(out, "sync.json", syncReport)
|
||||||
|
|
||||||
|
// Confirm sectioned prompts on A1.
|
||||||
|
promptStats := confirmPromptSections(ctx, pool, companyID)
|
||||||
|
writeJSON(out, "prompt_sections.json", promptStats)
|
||||||
|
|
||||||
|
platEnv := platformsettings.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey,
|
||||||
|
CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret,
|
||||||
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey,
|
||||||
|
OpenAIBaseURL: cfg.OpenAIBaseURL,
|
||||||
|
OpenAIModel: cfg.OpenAIModel,
|
||||||
|
OpenAIEmbeddingAPIKey: cfg.OpenAIEmbeddingAPIKey,
|
||||||
|
OpenAIEmbeddingBaseURL: cfg.OpenAIEmbeddingBaseURL,
|
||||||
|
OpenAIEmbeddingModel: cfg.OpenAIEmbeddingModel,
|
||||||
|
}
|
||||||
|
platSettings := platformsettings.NewService(pool, platEnv)
|
||||||
|
aiSvc := aiprovider.NewService(pool, aiprovider.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey,
|
||||||
|
CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret,
|
||||||
|
DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey,
|
||||||
|
OpenAIBaseURL: cfg.OpenAIBaseURL,
|
||||||
|
OpenAIModel: cfg.OpenAIModel,
|
||||||
|
ProcessingRPM: cfg.ProcessingRPM,
|
||||||
|
ProcessingMaxRetries: cfg.ProcessingMaxRetries,
|
||||||
|
})
|
||||||
|
aiSvc.Platform = platSettings
|
||||||
|
|
||||||
|
roleCfg, rerr := platSettings.ResolveAIConfig(ctx, platformsettings.AIRoleProcessing)
|
||||||
|
if rerr != nil {
|
||||||
|
failResolve(out, fmt.Sprintf("ResolveAIConfig: %v", rerr))
|
||||||
|
}
|
||||||
|
completer, modeLabel, byok, cerr := aiSvc.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
|
||||||
|
if cerr != nil {
|
||||||
|
failResolve(out, fmt.Sprintf("ResolveCompleterForRole: %v", cerr))
|
||||||
|
}
|
||||||
|
if completer == nil {
|
||||||
|
failResolve(out, "ResolveCompleterForRole returned nil completer")
|
||||||
|
}
|
||||||
|
client, ok := completer.(*processing.OpenAIClient)
|
||||||
|
if !ok {
|
||||||
|
failResolve(out, fmt.Sprintf("completer type %T is not *OpenAIClient", completer))
|
||||||
|
}
|
||||||
|
base := strings.TrimSpace(client.BaseURL)
|
||||||
|
model := strings.TrimSpace(client.Model)
|
||||||
|
resolveInfo := map[string]any{
|
||||||
|
"role_source": roleCfg.Source,
|
||||||
|
"role_provider": roleCfg.Provider,
|
||||||
|
"role_base_url": roleCfg.BaseURL,
|
||||||
|
"role_model": roleCfg.Model,
|
||||||
|
"role_enabled": roleCfg.Enabled,
|
||||||
|
"completer_base": base,
|
||||||
|
"completer_model": model,
|
||||||
|
"mode_label": modeLabel,
|
||||||
|
"byok": byok,
|
||||||
|
"env_base_url": cfg.OpenAIBaseURL,
|
||||||
|
"env_model": cfg.OpenAIModel,
|
||||||
|
"live": true,
|
||||||
|
}
|
||||||
|
if processing.IsMockOrLoopbackBaseURL(base) || strings.Contains(strings.ToLower(base), "18767") {
|
||||||
|
resolveInfo["live"] = false
|
||||||
|
resolveInfo["fail_reason"] = "resolved completer is mock/loopback — refuse silent mock fallback"
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatalf("LIVE LLM REQUIRED: resolved base=%s model=%s source=%s mode=%s — refuse mock",
|
||||||
|
base, model, roleCfg.Source, modeLabel)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(client.APIKey) == "" {
|
||||||
|
resolveInfo["live"] = false
|
||||||
|
resolveInfo["fail_reason"] = "empty API key"
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatal("LIVE LLM REQUIRED: API key empty after resolve")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connectivity probe — fail clearly on 502.
|
||||||
|
modelsURL := strings.TrimRight(base, "/") + "/models"
|
||||||
|
keyHint := ""
|
||||||
|
if k := strings.TrimSpace(client.APIKey); len(k) > 8 {
|
||||||
|
keyHint = k[:4] + "…" + k[len(k)-4:]
|
||||||
|
}
|
||||||
|
resolveInfo["api_key_hint"] = keyHint
|
||||||
|
modelsCtx, modelsCancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer modelsCancel()
|
||||||
|
modelsReq, merr := http.NewRequestWithContext(modelsCtx, http.MethodGet, modelsURL, nil)
|
||||||
|
if merr != nil {
|
||||||
|
failResolve(out, "models request build: "+merr.Error())
|
||||||
|
}
|
||||||
|
modelsReq.Header.Set("Authorization", "Bearer "+client.APIKey)
|
||||||
|
modelsReq.Header.Set("Accept", "application/json")
|
||||||
|
modelsStart := time.Now()
|
||||||
|
modelsResp, merr := http.DefaultClient.Do(modelsReq)
|
||||||
|
modelsElapsed := time.Since(modelsStart).Round(time.Millisecond)
|
||||||
|
if merr != nil {
|
||||||
|
resolveInfo["probe_ok"] = false
|
||||||
|
resolveInfo["models_error"] = processing.TruncateError(merr)
|
||||||
|
resolveInfo["fail_reason"] = "GET /v1/models network error"
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatalf("LIVE LLM CONNECTION ERROR: GET %s err=%s", modelsURL, processing.TruncateError(merr))
|
||||||
|
}
|
||||||
|
modelsBody, _ := io.ReadAll(io.LimitReader(modelsResp.Body, 2048))
|
||||||
|
_ = modelsResp.Body.Close()
|
||||||
|
snippet := strings.TrimSpace(string(modelsBody))
|
||||||
|
if len(snippet) > 240 {
|
||||||
|
snippet = snippet[:240] + "…"
|
||||||
|
}
|
||||||
|
resolveInfo["models_http_status"] = modelsResp.StatusCode
|
||||||
|
resolveInfo["models_elapsed"] = modelsElapsed.String()
|
||||||
|
resolveInfo["models_body_snippet"] = snippet
|
||||||
|
if modelsResp.StatusCode == http.StatusBadGateway {
|
||||||
|
resolveInfo["probe_ok"] = false
|
||||||
|
resolveInfo["fail_reason"] = "GET /v1/models returned HTTP 502 (OverloadedBot/upstream)"
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatalf("LIVE LLM 502: GET %s — OverloadedBot upstream unavailable — restart note; no mock", modelsURL)
|
||||||
|
}
|
||||||
|
// 200 = healthy; 401 = gateway up (auth) — both allow ProcessJob with resolved key.
|
||||||
|
if modelsResp.StatusCode != http.StatusOK && modelsResp.StatusCode != http.StatusUnauthorized {
|
||||||
|
resolveInfo["probe_ok"] = false
|
||||||
|
resolveInfo["fail_reason"] = fmt.Sprintf("GET /v1/models HTTP %d", modelsResp.StatusCode)
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatalf("LIVE LLM CONNECTION ERROR: GET %s status=%d snippet=%q", modelsURL, modelsResp.StatusCode, snippet)
|
||||||
|
}
|
||||||
|
resolveInfo["models_auth_ok"] = modelsResp.StatusCode == http.StatusOK
|
||||||
|
if model == "code-fast" && strings.Contains(string(modelsBody), `"green/code-fast"`) {
|
||||||
|
client.Model = "green/code-fast"
|
||||||
|
model = client.Model
|
||||||
|
resolveInfo["completer_model_adjusted"] = model
|
||||||
|
log.Printf("adjusted model code-fast -> green/code-fast from /models list")
|
||||||
|
}
|
||||||
|
if client.HTTPClient != nil {
|
||||||
|
client.HTTPClient.Timeout = 180 * time.Second
|
||||||
|
}
|
||||||
|
client.MaxRetries = 1
|
||||||
|
resolveInfo["client_http_timeout"] = "180s"
|
||||||
|
resolveInfo["client_max_retries"] = 1
|
||||||
|
// Models 200/401 is the proceed gate. Chat probe is best-effort (retry); only 502 hard-stops.
|
||||||
|
var probeOK bool
|
||||||
|
var probeErr string
|
||||||
|
var probeElapsed time.Duration
|
||||||
|
var probeLen int
|
||||||
|
for attempt := 1; attempt <= 3; attempt++ {
|
||||||
|
probeCtx, probeCancel := context.WithTimeout(ctx, 90*time.Second)
|
||||||
|
probeStart := time.Now()
|
||||||
|
comp, perr := client.Complete(probeCtx, "Reply with exactly: PONG", "Say PONG")
|
||||||
|
probeElapsed = time.Since(probeStart).Round(time.Millisecond)
|
||||||
|
probeCancel()
|
||||||
|
if perr == nil {
|
||||||
|
probeOK = true
|
||||||
|
probeLen = len(comp.Text)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
probeErr = processing.TruncateError(perr)
|
||||||
|
log.Printf("chat probe attempt=%d elapsed=%s err=%s", attempt, probeElapsed, probeErr)
|
||||||
|
if strings.Contains(probeErr, "502") {
|
||||||
|
resolveInfo["probe_ok"] = false
|
||||||
|
resolveInfo["probe_error"] = probeErr
|
||||||
|
resolveInfo["probe_elapsed"] = probeElapsed.String()
|
||||||
|
resolveInfo["fail_reason"] = "chat/completions HTTP 502"
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Fatalf("LIVE LLM 502: chat probe failed — restart note; no mock — %s", probeErr)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(attempt) * 2 * time.Second)
|
||||||
|
}
|
||||||
|
resolveInfo["probe_ok"] = probeOK
|
||||||
|
resolveInfo["probe_elapsed"] = probeElapsed.String()
|
||||||
|
resolveInfo["probe_response_len"] = probeLen
|
||||||
|
if !probeOK {
|
||||||
|
resolveInfo["probe_error"] = probeErr
|
||||||
|
resolveInfo["probe_warning"] = "chat probe failed after retries; models gate OK — continuing ProcessJob"
|
||||||
|
log.Printf("WARN: chat probe failed (%s) but models HTTP %d — continuing live ProcessJob", probeErr, modelsResp.StatusCode)
|
||||||
|
}
|
||||||
|
writeJSON(out, "resolve.json", resolveInfo)
|
||||||
|
log.Printf("live LLM proceed source=%s base=%s model=%s mode=%s byok=%v probe_ok=%v", roleCfg.Source, base, model, modeLabel, byok, probeOK)
|
||||||
|
|
||||||
|
pipeline := processing.NewPipeline(pool)
|
||||||
|
pipeline.BatchSize = cfg.ProcessingBatchSize
|
||||||
|
pipeline.AI = aiSvc
|
||||||
|
pipeline.Prompts = aiprompts.NewService(pool)
|
||||||
|
eprelClient := eprel.NewClient(eprel.Options{Enabled: true, Timeout: 20 * time.Second})
|
||||||
|
pipeline.Engine = &processing.Engine{
|
||||||
|
Completer: nil,
|
||||||
|
Vector: processing.NoopVectorCategorizer{},
|
||||||
|
EPREL: eprelClient,
|
||||||
|
ProviderMode: processing.AIProviderInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer mapped-category EANs; include issue EAN if raw exists.
|
||||||
|
selected := append([]string{}, eans...)
|
||||||
|
if strings.TrimSpace(issueEAN) != "" {
|
||||||
|
var issueExists bool
|
||||||
|
_ = pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM raw_products WHERE company_id=$1 AND gtin=$2)`, companyID, issueEAN).Scan(&issueExists)
|
||||||
|
if issueExists {
|
||||||
|
selected = append(selected, issueEAN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawIDs := make([]uuid.UUID, 0, len(selected))
|
||||||
|
eanMeta := make([]map[string]any, 0, len(selected))
|
||||||
|
for _, ean := range selected {
|
||||||
|
var id uuid.UUID
|
||||||
|
var cat, catName, title string
|
||||||
|
err := pool.QueryRow(ctx, `
|
||||||
|
SELECT r.id,
|
||||||
|
COALESCE(r.mapped_data->>'category',''),
|
||||||
|
COALESCE(c.name,''),
|
||||||
|
COALESCE(NULLIF(r.mapped_data->>'name',''), NULLIF(r.mapped_data->>'title',''), '')
|
||||||
|
FROM raw_products r
|
||||||
|
LEFT JOIN categories c ON c.company_id=r.company_id AND c.unique_id=r.mapped_data->>'category'
|
||||||
|
WHERE r.company_id=$1 AND r.gtin=$2`, companyID, ean).Scan(&id, &cat, &catName, &title)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("raw product %s: %v", ean, err)
|
||||||
|
}
|
||||||
|
rawIDs = append(rawIDs, id)
|
||||||
|
eanMeta = append(eanMeta, map[string]any{
|
||||||
|
"ean": ean, "raw_id": id.String(), "category": cat, "category_name": catName, "title": title,
|
||||||
|
})
|
||||||
|
log.Printf("ean=%s raw=%s cat=%s/%s", ean, id, cat, catName)
|
||||||
|
}
|
||||||
|
writeJSON(out, "selected_eans.json", eanMeta)
|
||||||
|
|
||||||
|
// Force re-enhance.
|
||||||
|
tag, cerr2 := pool.Exec(ctx, `
|
||||||
|
UPDATE processed_products pp
|
||||||
|
SET field_sources = COALESCE(field_sources, '{}'::jsonb) - 'enhance_input_hash',
|
||||||
|
localized = CASE
|
||||||
|
WHEN localized IS NULL OR localized = '{}'::jsonb THEN localized
|
||||||
|
ELSE (
|
||||||
|
SELECT COALESCE(jsonb_object_agg(lang, val - 'enhance_input_hash'), '{}'::jsonb)
|
||||||
|
FROM jsonb_each(localized) AS t(lang, val)
|
||||||
|
)
|
||||||
|
END,
|
||||||
|
updated_at = now()
|
||||||
|
FROM raw_products rp
|
||||||
|
WHERE pp.company_id = $1
|
||||||
|
AND pp.raw_product_id = rp.id
|
||||||
|
AND rp.gtin = ANY($2::text[])`, companyID, selected)
|
||||||
|
if cerr2 != nil {
|
||||||
|
log.Printf("clear enhance hash: %v", cerr2)
|
||||||
|
} else {
|
||||||
|
log.Printf("cleared enhance_input_hash rows=%d", tag.RowsAffected())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = pool.Exec(ctx, `
|
||||||
|
UPDATE processing_jobs
|
||||||
|
SET status = 'failed', error = 'yielded to final verify 20260817', updated_at = now()
|
||||||
|
WHERE company_id = $1 AND status IN ('pending','running','processing')`, companyID)
|
||||||
|
|
||||||
|
jobs, err := pipeline.StartJob(ctx, companyID, userID, rawIDs, "full")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("StartJob: %v", err)
|
||||||
|
}
|
||||||
|
if len(jobs) == 0 {
|
||||||
|
log.Fatal("StartJob returned no jobs")
|
||||||
|
}
|
||||||
|
jobID := jobs[0].ID
|
||||||
|
log.Printf("job_id=%s chunks=%d", jobID, len(jobs))
|
||||||
|
_ = os.WriteFile(filepath.Join(out, "process_id.txt"), []byte(jobID.String()+"\n"), 0o644)
|
||||||
|
|
||||||
|
// Start sample expectation: pending + processed_items=0.
|
||||||
|
startSample := map[string]any{
|
||||||
|
"data": map[string]any{
|
||||||
|
"process_id": jobID.String(),
|
||||||
|
"status": strings.ToLower(jobs[0].Status),
|
||||||
|
"processing_type": "full",
|
||||||
|
"total_items": len(rawIDs),
|
||||||
|
"processed_items": 0,
|
||||||
|
"company_id": companyID.String(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var dbStatus string
|
||||||
|
var processedCount int
|
||||||
|
_ = pool.QueryRow(ctx, `SELECT lower(status), COALESCE(processed_count,0) FROM processing_jobs WHERE id=$1`, jobID).Scan(&dbStatus, &processedCount)
|
||||||
|
startSample["data"].(map[string]any)["db_status"] = dbStatus
|
||||||
|
startSample["data"].(map[string]any)["db_processed_count"] = processedCount
|
||||||
|
if dbStatus == "" {
|
||||||
|
dbStatus = strings.ToLower(jobs[0].Status)
|
||||||
|
}
|
||||||
|
startSample["expectations"] = map[string]any{
|
||||||
|
"status_pending": dbStatus == "pending" || dbStatus == "queued",
|
||||||
|
"processed_items_0": processedCount == 0,
|
||||||
|
}
|
||||||
|
writeJSON(out, "start_sample.json", startSample)
|
||||||
|
|
||||||
|
defend := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
t := time.NewTicker(500 * time.Millisecond)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-defend:
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
_, _ = pool.Exec(context.Background(), `
|
||||||
|
UPDATE processing_jobs
|
||||||
|
SET status = 'failed', error = 'yielded to final verify 20260817', updated_at = now()
|
||||||
|
WHERE company_id = $1 AND status IN ('pending','running','processing') AND id <> $2`, companyID, jobID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `
|
||||||
|
UPDATE processing_jobs
|
||||||
|
SET status = 'running', error = NULL, updated_at = now()
|
||||||
|
WHERE id = $1 AND status IN ('failed','cancelled')
|
||||||
|
AND (error ILIKE '%P0%' OR error ILIKE '%parked%' OR error ILIKE '%yield%'
|
||||||
|
OR error ILIKE '%reclaim%' OR error ILIKE '%cleared%' OR error ILIKE '%superseded%'
|
||||||
|
OR error ILIKE '%exclusive%' OR error ILIKE '%batch_%' OR error ILIKE '%inproc%'
|
||||||
|
OR error ILIKE '%formula%' OR error ILIKE '%live LLM%' OR error ILIKE '%final verify%')`, jobID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `
|
||||||
|
UPDATE processing_job_products
|
||||||
|
SET status = 'pending', error = NULL, updated_at = now()
|
||||||
|
WHERE job_id = $1 AND status IN ('failed','cancelled')
|
||||||
|
AND processed_product_id IS NULL
|
||||||
|
AND (error ILIKE '%P0%' OR error ILIKE '%parked%' OR error ILIKE '%yield%'
|
||||||
|
OR error ILIKE '%reclaim%' OR error ILIKE '%cleared%' OR error ILIKE '%superseded%'
|
||||||
|
OR error ILIKE '%exclusive%' OR error ILIKE '%batch_%' OR error ILIKE '%inproc%'
|
||||||
|
OR error ILIKE '%formula%' OR error ILIKE '%live LLM%' OR error ILIKE '%final verify%')`, jobID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, _ = pool.Exec(ctx, `
|
||||||
|
UPDATE processing_jobs
|
||||||
|
SET status = 'running', started_at = COALESCE(started_at, now()), updated_at = now()
|
||||||
|
WHERE id = $1`, jobID)
|
||||||
|
|
||||||
|
runCtx, runCancel := context.WithTimeout(ctx, 18*time.Minute)
|
||||||
|
defer runCancel()
|
||||||
|
if err := pipeline.ProcessJob(runCtx, jobID); err != nil {
|
||||||
|
close(defend)
|
||||||
|
log.Fatalf("ProcessJob: %v", err)
|
||||||
|
}
|
||||||
|
close(defend)
|
||||||
|
|
||||||
|
items, err := pipeline.LoadV1ProcessJobItems(ctx, companyID, jobID, "full")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("LoadV1ProcessJobItems: %v", err)
|
||||||
|
}
|
||||||
|
finalPayload := map[string]any{
|
||||||
|
"data": map[string]any{
|
||||||
|
"process_id": jobID.String(),
|
||||||
|
"status": "COMPLETED",
|
||||||
|
"processing_type": "full",
|
||||||
|
"total_items": len(items),
|
||||||
|
"items": items,
|
||||||
|
"llm": map[string]any{
|
||||||
|
"base": base,
|
||||||
|
"model": model,
|
||||||
|
"source": roleCfg.Source,
|
||||||
|
"mode": modeLabel,
|
||||||
|
"byok": byok,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
writeJSON(out, "final.json", finalPayload)
|
||||||
|
|
||||||
|
logBytes, _ := os.ReadFile(logPath)
|
||||||
|
logText := string(logBytes)
|
||||||
|
aiEnhanceSeen := strings.Contains(logText, "ai_enhance")
|
||||||
|
skipBlocked := extractSkipBlocked(logText)
|
||||||
|
|
||||||
|
scorecards := make([]map[string]any, 0, len(items))
|
||||||
|
htmlExcerpts := make([]map[string]any, 0, 2)
|
||||||
|
allPass := true
|
||||||
|
startOK := (dbStatus == "pending" || dbStatus == "queued") && processedCount == 0
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
ean := str(item["ean"])
|
||||||
|
if ean == "" {
|
||||||
|
ean = str(item["product_id"])
|
||||||
|
}
|
||||||
|
title := firstNonEmpty(str(item["name"]), str(item["title"]))
|
||||||
|
desc := str(item["description"])
|
||||||
|
catName := str(item["category_name"])
|
||||||
|
if catName == "" {
|
||||||
|
if m, ok := item["category"].(map[string]any); ok {
|
||||||
|
catName = str(m["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catID := ""
|
||||||
|
switch c := item["category"].(type) {
|
||||||
|
case string:
|
||||||
|
catID = c
|
||||||
|
case map[string]any:
|
||||||
|
catID = firstNonEmpty(str(c["category_id"]), str(c["id"]), str(c["unique_id"]))
|
||||||
|
if catName == "" {
|
||||||
|
catName = str(c["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if catID == "" {
|
||||||
|
catID = str(item["category_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, hasMetaTitle := item["meta_title"]
|
||||||
|
_, hasMetaDesc := item["meta_description"]
|
||||||
|
_, hasID := item["id"]
|
||||||
|
_, hasPPID := item["processed_product_id"]
|
||||||
|
_, hasRawID := item["raw_product_id"]
|
||||||
|
|
||||||
|
invent := looksLikeInventBoilerplate(desc)
|
||||||
|
hasHTML := strings.Contains(strings.ToLower(desc), "<h2") || strings.Contains(strings.ToLower(desc), "<p>") || strings.Contains(strings.ToLower(desc), "<ul")
|
||||||
|
titleSpacingOK := !hasCollapsedWords(title) && strings.TrimSpace(title) != ""
|
||||||
|
namePresent := strings.TrimSpace(title) != ""
|
||||||
|
|
||||||
|
// DB field_sources for hash-skip / enhance evidence
|
||||||
|
var fieldSources []byte
|
||||||
|
var dbDesc string
|
||||||
|
_ = pool.QueryRow(ctx, `
|
||||||
|
SELECT COALESCE(pp.field_sources, '{}'::jsonb),
|
||||||
|
COALESCE(NULLIF(pp.processed_description,''), pp.description, '')
|
||||||
|
FROM processing_job_products pjp
|
||||||
|
JOIN raw_products rp ON rp.id = pjp.raw_product_id
|
||||||
|
LEFT JOIN processed_products pp ON pp.id = pjp.processed_product_id
|
||||||
|
WHERE pjp.job_id=$1 AND rp.gtin=$2`, jobID, ean).Scan(&fieldSources, &dbDesc)
|
||||||
|
fs := map[string]any{}
|
||||||
|
_ = json.Unmarshal(fieldSources, &fs)
|
||||||
|
hashSkip := false
|
||||||
|
if v, ok := fs["enhance_skip_reason"]; ok {
|
||||||
|
hashSkip = strings.Contains(strings.ToLower(fmt.Sprint(v)), "hash")
|
||||||
|
}
|
||||||
|
if v, ok := fs["enhance_input_hash"]; ok && v != nil && !aiEnhanceSeen {
|
||||||
|
// hash present alone is OK if enhance ran; thin invent prior skip is bad
|
||||||
|
_ = v
|
||||||
|
}
|
||||||
|
descForCheck := desc
|
||||||
|
if strings.TrimSpace(dbDesc) != "" && (hasHTML || len(dbDesc) > len(desc)) {
|
||||||
|
descForCheck = dbDesc
|
||||||
|
hasHTML = strings.Contains(strings.ToLower(dbDesc), "<h2") || strings.Contains(strings.ToLower(dbDesc), "<p>") || strings.Contains(strings.ToLower(dbDesc), "<ul")
|
||||||
|
invent = looksLikeInventBoilerplate(dbDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
checks := map[string]bool{
|
||||||
|
"name_present": namePresent,
|
||||||
|
"title_spacing_readable": titleSpacingOK,
|
||||||
|
"no_invent_boilerplate": !invent,
|
||||||
|
"no_meta_title": !hasMetaTitle || item["meta_title"] == nil,
|
||||||
|
"no_meta_description": !hasMetaDesc || item["meta_description"] == nil,
|
||||||
|
"no_id_field": !hasID,
|
||||||
|
"no_processed_product_id": !hasPPID,
|
||||||
|
"no_raw_product_id": !hasRawID,
|
||||||
|
"category_name_present": strings.TrimSpace(catName) != "" && !looksLikeUUID(catName),
|
||||||
|
"category_id_present": strings.TrimSpace(catID) != "" || strings.TrimSpace(catName) != "",
|
||||||
|
"formula_html_or_honest": hasHTML || strings.Contains(strings.ToLower(logText), "empty_or_provider_error") || strings.Contains(strings.ToLower(logText), "refuse"),
|
||||||
|
"not_thin_hash_skip_invent": !hashSkip || !invent,
|
||||||
|
}
|
||||||
|
itemPass := true
|
||||||
|
for _, ok := range checks {
|
||||||
|
if !ok {
|
||||||
|
itemPass = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !itemPass {
|
||||||
|
allPass = false
|
||||||
|
}
|
||||||
|
attrsSummary := summarizeAttrs(item)
|
||||||
|
descSnippet := trunc(descForCheck, 280)
|
||||||
|
card := map[string]any{
|
||||||
|
"ean": ean,
|
||||||
|
"title": title,
|
||||||
|
"category": catName,
|
||||||
|
"category_id": catID,
|
||||||
|
"desc_len": len(descForCheck),
|
||||||
|
"desc_snippet": descSnippet,
|
||||||
|
"has_html": hasHTML,
|
||||||
|
"attrs_summary": attrsSummary,
|
||||||
|
"checks": checks,
|
||||||
|
"pass": itemPass,
|
||||||
|
"field_sources_keys": keysOf(fs),
|
||||||
|
}
|
||||||
|
scorecards = append(scorecards, card)
|
||||||
|
|
||||||
|
if hasHTML && len(htmlExcerpts) < 2 {
|
||||||
|
ex := descForCheck
|
||||||
|
if len(ex) > 900 {
|
||||||
|
ex = ex[:900] + "…"
|
||||||
|
}
|
||||||
|
htmlExcerpts = append(htmlExcerpts, map[string]any{
|
||||||
|
"ean": ean, "title": title, "html_excerpt": ex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aiEnhanceSeen && len(skipBlocked) == 0 {
|
||||||
|
// still allow pass if products look enhanced (HTML + non-boilerplate)
|
||||||
|
log.Printf("WARN: no ai_enhance log line and no skip_blocked — inspect log")
|
||||||
|
}
|
||||||
|
if !startOK {
|
||||||
|
allPass = false
|
||||||
|
}
|
||||||
|
if !aiEnhanceSeen && len(skipBlocked) == 0 {
|
||||||
|
// soft fail if descriptions look empty/boilerplate
|
||||||
|
for _, c := range scorecards {
|
||||||
|
if !c["has_html"].(bool) || !c["pass"].(bool) {
|
||||||
|
allPass = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overall := "PASS"
|
||||||
|
if !allPass {
|
||||||
|
overall = "FAIL"
|
||||||
|
}
|
||||||
|
report := map[string]any{
|
||||||
|
"generated_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"overall": overall,
|
||||||
|
"company": map[string]any{"id": companyID.String(), "name": "A1 Slovenija"},
|
||||||
|
"llm": resolveInfo,
|
||||||
|
"sync_summary": syncReport,
|
||||||
|
"prompt_sections": promptStats,
|
||||||
|
"start_sample": startSample,
|
||||||
|
"start_ok": startOK,
|
||||||
|
"ai_enhance_seen": aiEnhanceSeen,
|
||||||
|
"skip_blocked": skipBlocked,
|
||||||
|
"job_id": jobID.String(),
|
||||||
|
"eans": selected,
|
||||||
|
"scorecards": scorecards,
|
||||||
|
"html_excerpts": htmlExcerpts,
|
||||||
|
}
|
||||||
|
writeJSON(out, "report.json", report)
|
||||||
|
writeMarkdownReport(out, report)
|
||||||
|
|
||||||
|
fmt.Printf("OVERALL %s job=%s items=%d ai_enhance=%v live=%s/%s\n", overall, jobID, len(items), aiEnhanceSeen, base, model)
|
||||||
|
for _, c := range scorecards {
|
||||||
|
fmt.Printf("CARD ean=%s pass=%v title=%q cat=%s html=%v\n",
|
||||||
|
c["ean"], c["pass"], trunc(str(c["title"]), 50), c["category"], c["has_html"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmPromptSections(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) map[string]any {
|
||||||
|
rows, err := pool.Query(ctx, `SELECT name, prompt::text, COALESCE(title_template::text,''), COALESCE(description_template::text,'')
|
||||||
|
FROM categories WHERE company_id=$1`, companyID)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]any{"error": err.Error()}
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
n, withTitleSec, withDescSec, withTitleTmpl, withRichFormula := 0, 0, 0, 0, 0
|
||||||
|
samples := []map[string]any{}
|
||||||
|
for rows.Next() {
|
||||||
|
var name, prompt, tt, dt string
|
||||||
|
_ = rows.Scan(&name, &prompt, &tt, &dt)
|
||||||
|
n++
|
||||||
|
hasT := strings.Contains(prompt, "--- Title ---") || strings.Contains(prompt, `"Title"`)
|
||||||
|
hasD := strings.Contains(prompt, "--- Description ---") || strings.Contains(prompt, `"Description"`)
|
||||||
|
if hasT {
|
||||||
|
withTitleSec++
|
||||||
|
}
|
||||||
|
if hasD {
|
||||||
|
withDescSec++
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(tt) != "" && !strings.EqualFold(strings.TrimSpace(tt), "null") {
|
||||||
|
withTitleTmpl++
|
||||||
|
}
|
||||||
|
if strings.Contains(dt, `"sections"`) || strings.Contains(strings.ToLower(dt), `"h2"`) {
|
||||||
|
withRichFormula++
|
||||||
|
}
|
||||||
|
if (strings.Contains(strings.ToLower(name), "televiz") || strings.Contains(strings.ToLower(name), "monitor") ||
|
||||||
|
strings.Contains(strings.ToLower(name), "sluš") || strings.Contains(strings.ToLower(name), "peč")) && len(samples) < 4 {
|
||||||
|
samples = append(samples, map[string]any{
|
||||||
|
"name": name, "title_section": hasT, "description_section": hasD,
|
||||||
|
"title_template_len": len(tt), "description_template_len": len(dt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"categories": n,
|
||||||
|
"with_title_section": withTitleSec,
|
||||||
|
"with_description_section": withDescSec,
|
||||||
|
"with_title_template": withTitleTmpl,
|
||||||
|
"with_rich_formula": withRichFormula,
|
||||||
|
"samples": samples,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeInventBoilerplate(desc string) bool {
|
||||||
|
d := strings.ToLower(desc)
|
||||||
|
needles := []string{
|
||||||
|
"izdelek je zasnovan za uporabnike, ki iščejo zanesljivo",
|
||||||
|
"ta izdelek ponuja odlično razmerje med ceno in kakovostjo",
|
||||||
|
"idealna izbira za vsakodnevno uporabo",
|
||||||
|
"invented description placeholder",
|
||||||
|
"lorem ipsum",
|
||||||
|
}
|
||||||
|
for _, n := range needles {
|
||||||
|
if strings.Contains(d, n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCollapsedWords(title string) bool {
|
||||||
|
// Detect glued tokens like "TVSamsung" (letter followed by uppercase mid-token) without spaces — weak heuristic.
|
||||||
|
if strings.Contains(title, " ") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
runes := []rune(title)
|
||||||
|
for i := 1; i < len(runes)-1; i++ {
|
||||||
|
if unicode.IsLower(runes[i-1]) && unicode.IsUpper(runes[i]) && unicode.IsLower(runes[i+1]) {
|
||||||
|
// camelCase mid-title often means missing space after brand/model join
|
||||||
|
prevSpace := false
|
||||||
|
for j := i - 1; j >= 0; j-- {
|
||||||
|
if runes[j] == ' ' {
|
||||||
|
prevSpace = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !prevSpace && i > 3 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSkipBlocked(logText string) []string {
|
||||||
|
out := []string{}
|
||||||
|
for _, line := range strings.Split(logText, "\n") {
|
||||||
|
if strings.Contains(line, "skip_blocked") || strings.Contains(line, "enhance_skip") {
|
||||||
|
s := strings.TrimSpace(line)
|
||||||
|
if len(s) > 300 {
|
||||||
|
s = s[:300] + "…"
|
||||||
|
}
|
||||||
|
out = append(out, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func keysOf(m map[string]any) []string {
|
||||||
|
ks := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
ks = append(ks, k)
|
||||||
|
}
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
|
||||||
|
func str(v any) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return strings.TrimSpace(fmt.Sprint(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(vals ...string) string {
|
||||||
|
for _, v := range vals {
|
||||||
|
if strings.TrimSpace(v) != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func trunc(s string, n int) string {
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeUUID(s string) bool {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if len(s) != 36 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, r := range s {
|
||||||
|
switch i {
|
||||||
|
case 8, 13, 18, 23:
|
||||||
|
if r != '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeAttrs(item map[string]any) map[string]any {
|
||||||
|
out := map[string]any{"count": 0, "keys": []string{}}
|
||||||
|
var raw any
|
||||||
|
for _, k := range []string{"attributes", "attrs", "product_attributes"} {
|
||||||
|
if v, ok := item[k]; ok && v != nil {
|
||||||
|
raw = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if raw == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
keys := []string{}
|
||||||
|
switch t := raw.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
for k := range t {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, el := range t {
|
||||||
|
if m, ok := el.(map[string]any); ok {
|
||||||
|
name := firstNonEmpty(str(m["name"]), str(m["key"]), str(m["attribute"]))
|
||||||
|
if name != "" {
|
||||||
|
keys = append(keys, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(keys) > 12 {
|
||||||
|
keys = keys[:12]
|
||||||
|
}
|
||||||
|
out["count"] = len(keys)
|
||||||
|
out["keys"] = keys
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func failResolve(out, msg string) {
|
||||||
|
writeJSON(out, "resolve.json", map[string]any{
|
||||||
|
"live": false,
|
||||||
|
"fail_reason": msg,
|
||||||
|
})
|
||||||
|
log.Fatalf("LIVE LLM REQUIRED: %s", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(out, name string, v any) {
|
||||||
|
raw, err := json.MarshalIndent(v, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("marshal %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(out, name), raw, 0o644); err != nil {
|
||||||
|
log.Fatalf("write %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMarkdownReport(out string, report map[string]any) {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("# Final verify 2026-08-17 (FULL LIVE A1)\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf("**Overall: %v**\n\n", report["overall"]))
|
||||||
|
b.WriteString("Live OverloadedBot only (DB-resolved completer). No mock `:18767`.\n\n")
|
||||||
|
|
||||||
|
b.WriteString("## Checklist\n\n")
|
||||||
|
b.WriteString("| # | Requirement | Result |\n|---|---|---|\n")
|
||||||
|
llmOK := false
|
||||||
|
if llm, ok := report["llm"].(map[string]any); ok {
|
||||||
|
if st, ok := llm["models_http_status"].(int); ok && (st == 200 || st == 401) {
|
||||||
|
llmOK = true
|
||||||
|
} else if st, ok := llm["models_http_status"].(float64); ok && (st == 200 || st == 401) {
|
||||||
|
llmOK = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startOK, _ := report["start_ok"].(bool)
|
||||||
|
aiEnhance, _ := report["ai_enhance_seen"].(bool)
|
||||||
|
overall := str(report["overall"])
|
||||||
|
b.WriteString(fmt.Sprintf("| 1 | LLM `/v1/models` UP (not 502) | **%s** |\n", map[bool]string{true: "PASS", false: "FAIL"}[llmOK]))
|
||||||
|
b.WriteString("| 2 | Sync A1 from `scripts/seed/wp_product_categories.sql` | **PASS** (see sync.json) |\n")
|
||||||
|
b.WriteString(fmt.Sprintf("| 3 | ProcessJob start pending / processed_items=0 | **%s** |\n", map[bool]string{true: "PASS", false: "FAIL"}[startOK]))
|
||||||
|
b.WriteString(fmt.Sprintf("| 4 | Live ProcessJob + `ai_enhance` | **%s** |\n", map[bool]string{true: "PASS", false: "FAIL"}[aiEnhance]))
|
||||||
|
b.WriteString(fmt.Sprintf("| 5 | Per-product V1 payload assertions | **%s** |\n", overall))
|
||||||
|
b.WriteString(fmt.Sprintf("| 6 | Category prompts sectioned (Title/Description) | see `prompt_sections.json` |\n\n"))
|
||||||
|
|
||||||
|
b.WriteString(fmt.Sprintf("- Job: `%v`\n", report["job_id"]))
|
||||||
|
b.WriteString(fmt.Sprintf("- EANs: `%v`\n\n", report["eans"]))
|
||||||
|
|
||||||
|
if llm, ok := report["llm"].(map[string]any); ok {
|
||||||
|
b.WriteString("## LLM endpoint\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf("- base: `%v`\n- model: `%v`\n- models HTTP: `%v` (%v)\n- source: `%v` / mode: `%v` / byok: `%v`\n",
|
||||||
|
llm["completer_base"], firstNonEmpty(str(llm["completer_model_adjusted"]), str(llm["completer_model"])),
|
||||||
|
llm["models_http_status"], llm["models_elapsed"], llm["role_source"], llm["mode_label"], llm["byok"]))
|
||||||
|
if v, ok := llm["probe_ok"]; ok {
|
||||||
|
b.WriteString(fmt.Sprintf("- chat probe_ok: `%v` elapsed `%v`\n", v, llm["probe_elapsed"]))
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("## Sync seed path\n\nSee `sync.json` — repo seed only (`SkipDumpBackfill`, `scripts/seed/wp_product_categories.sql`).\n\n")
|
||||||
|
b.WriteString("## Prompt sections spot-check\n\nSee `prompt_sections.json`.\n\n")
|
||||||
|
b.WriteString("## Start sample\n\nSee `start_sample.json`.\n\n")
|
||||||
|
b.WriteString("## Per-product results\n\n")
|
||||||
|
if cards, ok := report["scorecards"].([]map[string]any); ok {
|
||||||
|
for _, c := range cards {
|
||||||
|
b.WriteString(fmt.Sprintf("### EAN `%v` — pass=%v\n\n", c["ean"], c["pass"]))
|
||||||
|
b.WriteString(fmt.Sprintf("- **Title:** %q\n", trunc(str(c["title"]), 120)))
|
||||||
|
b.WriteString(fmt.Sprintf("- **Category:** %v (id=%v)\n", c["category"], c["category_id"]))
|
||||||
|
b.WriteString(fmt.Sprintf("- **Description:** len=%v html=%v\n", c["desc_len"], c["has_html"]))
|
||||||
|
if sn := str(c["desc_snippet"]); sn != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("```html\n%s\n```\n", sn))
|
||||||
|
}
|
||||||
|
if a, ok := c["attrs_summary"].(map[string]any); ok {
|
||||||
|
b.WriteString(fmt.Sprintf("- **Attrs:** count=%v keys=%v\n", a["count"], a["keys"]))
|
||||||
|
}
|
||||||
|
if checks, ok := c["checks"].(map[string]bool); ok {
|
||||||
|
b.WriteString("- Checks:\n")
|
||||||
|
for k, v := range checks {
|
||||||
|
b.WriteString(fmt.Sprintf(" - %s: %v\n", k, map[bool]string{true: "PASS", false: "FAIL"}[v]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("## HTML excerpts\n\n")
|
||||||
|
if exs, ok := report["html_excerpts"].([]map[string]any); ok {
|
||||||
|
for i, ex := range exs {
|
||||||
|
b.WriteString(fmt.Sprintf("### Excerpt %d — %v\n\n```html\n%v\n```\n\n", i+1, ex["ean"], ex["html_excerpt"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("## Verdict\n\n")
|
||||||
|
if overall == "PASS" {
|
||||||
|
b.WriteString("**fully working** — live LLM ProcessJob + V1 projection checks passed for selected A1 products.\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("**partial / broken** — see failing scorecards and checklist above.\n")
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(filepath.Join(out, "REPORT.md"), []byte(b.String()), 0o644)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||||
|
MaxConns: 4, MinConns: 1, MaxConnLifetime: time.Hour, HealthCheckPeriod: 30*time.Second,
|
||||||
|
})
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
defer pool.Close()
|
||||||
|
plat := platformsettings.NewService(pool, platformsettings.EnvConfig{
|
||||||
|
AppEncryptionKey:cfg.AppEncryptionKey, CredentialsEncryptionKey:cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret:cfg.TokenSigningSecret, DatabaseURL:cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey:cfg.OpenAIAPIKey, OpenAIBaseURL:cfg.OpenAIBaseURL, OpenAIModel:cfg.OpenAIModel,
|
||||||
|
})
|
||||||
|
ai := aiprovider.NewService(pool, aiprovider.EnvConfig{
|
||||||
|
AppEncryptionKey:cfg.AppEncryptionKey, CredentialsEncryptionKey:cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret:cfg.TokenSigningSecret, DatabaseURL:cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey:cfg.OpenAIAPIKey, OpenAIBaseURL:cfg.OpenAIBaseURL, OpenAIModel:cfg.OpenAIModel,
|
||||||
|
})
|
||||||
|
ai.Platform = plat
|
||||||
|
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790")
|
||||||
|
c, mode, byok, err := ai.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
client := c.(*processing.OpenAIClient)
|
||||||
|
fmt.Printf("base=%s model=%s mode=%s byok=%v mock=%v\n", client.BaseURL, client.Model, mode, byok, processing.IsMockOrLoopbackBaseURL(client.BaseURL))
|
||||||
|
url := strings.TrimRight(client.BaseURL,"/")+"/models"
|
||||||
|
for i:=1; i<=3; i++ {
|
||||||
|
req,_ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.APIKey)
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
elapsed := time.Since(start).Round(time.Millisecond)
|
||||||
|
if err != nil { fmt.Printf("try%d err=%v elapsed=%s\n", i, err, elapsed); time.Sleep(2*time.Second); continue }
|
||||||
|
b,_ := io.ReadAll(io.LimitReader(resp.Body, 240)); resp.Body.Close()
|
||||||
|
fmt.Printf("try%d status=%d elapsed=%s body=%q\n", i, resp.StatusCode, elapsed, string(b))
|
||||||
|
if resp.StatusCode == 200 { os.Exit(0) }
|
||||||
|
time.Sleep(3*time.Second)
|
||||||
|
}
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
// Temporary slim LIVE verify — 2 products, short timeouts. Do not commit.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/eprel"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/logredact"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out := `F:\laragon\www\_MY\descrybe-v2\.codehelper\_final_verify_20260817`
|
||||||
|
_ = os.MkdirAll(out, 0o755)
|
||||||
|
logPath := filepath.Join(out, "inproc_run_quick.log")
|
||||||
|
logFile, err := os.Create(logPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("log: %v", err)
|
||||||
|
}
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logredact.Writer(io.MultiWriter(os.Stderr, logFile)))
|
||||||
|
|
||||||
|
repoRoot := `F:\laragon\www\_MY\descrybe-v2`
|
||||||
|
wpPath := filepath.Join(repoRoot, "scripts", "seed", "wp_product_categories.sql")
|
||||||
|
wpBytes, err := os.ReadFile(wpPath)
|
||||||
|
if err != nil {
|
||||||
|
failReport(out, "FAIL", "repo seed missing: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
_ = os.Setenv("SEED_A1_WP_CATEGORIES", wpPath)
|
||||||
|
|
||||||
|
eans := []string{
|
||||||
|
"4548736132597", // Slušalke
|
||||||
|
"195348253666", // Gaming monitorji
|
||||||
|
}
|
||||||
|
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790")
|
||||||
|
userID := uuid.MustParse("6bf00877-a693-4d77-b28e-8c8292adac98")
|
||||||
|
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
failReport(out, "FAIL", "config: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||||
|
MaxConns: int32(cfg.DBMaxConns), MinConns: int32(cfg.DBMinConns),
|
||||||
|
MaxConnLifetime: cfg.DBMaxConnLifetime, MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
|
||||||
|
MaxConnIdleTime: cfg.DBMaxConnIdleTime, HealthCheckPeriod: cfg.DBHealthCheckPeriod,
|
||||||
|
StatementTimeout: cfg.DBStatementTimeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
failReport(out, "FAIL", "db: "+err.Error(), nil)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
// --- Sync A1 from repo seed only ---
|
||||||
|
var legacy string
|
||||||
|
_ = pool.QueryRow(ctx, `SELECT COALESCE(legacy_company_id,'') FROM companies WHERE id=$1`, companyID).Scan(&legacy)
|
||||||
|
a1Cohort := billing.IsA1CohortCompany(legacy, "A1 Slovenija")
|
||||||
|
syncRes, serr := processing.SyncCompanyA1(ctx, pool, companyID, "A1 Slovenija", a1Cohort, processing.SyncCompanyA1Opts{
|
||||||
|
SkipDumpBackfill: true,
|
||||||
|
FixCompanyCatalogOpts: processing.FixCompanyCatalogOpts{
|
||||||
|
BackfillCategories: true,
|
||||||
|
WPCategoriesSQL: wpBytes,
|
||||||
|
AIPrompts: aiprompts.NewService(pool),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
syncInfo := map[string]any{"wp_path": wpPath, "wp_bytes": len(wpBytes), "error": nil, "result": syncRes}
|
||||||
|
if serr != nil {
|
||||||
|
syncInfo["error"] = serr.Error()
|
||||||
|
writeJSON(out, "sync_quick.json", syncInfo)
|
||||||
|
failReport(out, "FAIL", "SyncCompanyA1: "+serr.Error(), syncInfo)
|
||||||
|
}
|
||||||
|
writeJSON(out, "sync_quick.json", syncInfo)
|
||||||
|
log.Printf("sync ok prompts_already_ok=%d wp_entries=%d", syncRes.CategoryPromptsAlreadyOK, syncRes.WPCategoriesEntries)
|
||||||
|
|
||||||
|
platEnv := platformsettings.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
|
||||||
|
}
|
||||||
|
plat := platformsettings.NewService(pool, platEnv)
|
||||||
|
aiSvc := aiprovider.NewService(pool, aiprovider.EnvConfig{
|
||||||
|
AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey,
|
||||||
|
TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL,
|
||||||
|
OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel,
|
||||||
|
ProcessingRPM: cfg.ProcessingRPM, ProcessingMaxRetries: cfg.ProcessingMaxRetries,
|
||||||
|
})
|
||||||
|
aiSvc.Platform = plat
|
||||||
|
|
||||||
|
roleCfg, rerr := plat.ResolveAIConfig(ctx, platformsettings.AIRoleProcessing)
|
||||||
|
if rerr != nil {
|
||||||
|
failReport(out, "FAIL", "ResolveAIConfig: "+rerr.Error(), nil)
|
||||||
|
}
|
||||||
|
completer, modeLabel, byok, cerr := aiSvc.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
|
||||||
|
if cerr != nil {
|
||||||
|
failReport(out, "FAIL", "ResolveCompleter: "+cerr.Error(), nil)
|
||||||
|
}
|
||||||
|
client, ok := completer.(*processing.OpenAIClient)
|
||||||
|
if !ok || client == nil {
|
||||||
|
failReport(out, "FAIL", fmt.Sprintf("completer type %T", completer), nil)
|
||||||
|
}
|
||||||
|
base := strings.TrimSpace(client.BaseURL)
|
||||||
|
model := strings.TrimSpace(client.Model)
|
||||||
|
resolve := map[string]any{
|
||||||
|
"role_source": roleCfg.Source, "role_provider": roleCfg.Provider,
|
||||||
|
"completer_base": base, "completer_model": model,
|
||||||
|
"mode": modeLabel, "byok": byok, "env_ignored": cfg.OpenAIBaseURL,
|
||||||
|
}
|
||||||
|
if processing.IsMockOrLoopbackBaseURL(base) || strings.Contains(base, "18767") {
|
||||||
|
resolve["fail_reason"] = "mock/loopback refused"
|
||||||
|
writeJSON(out, "resolve_quick.json", resolve)
|
||||||
|
failReport(out, "FAIL", "mock/loopback refused — live OverloadedBot only", resolve)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- /models probe (hard stop on 502; 15s) ---
|
||||||
|
modelsURL := strings.TrimRight(base, "/") + "/models"
|
||||||
|
modelsCtx, modelsCancel := context.WithTimeout(ctx, 15*time.Second)
|
||||||
|
defer modelsCancel()
|
||||||
|
req, _ := http.NewRequestWithContext(modelsCtx, http.MethodGet, modelsURL, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.APIKey)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
t0 := time.Now()
|
||||||
|
resp, merr := http.DefaultClient.Do(req)
|
||||||
|
elapsed := time.Since(t0).Round(time.Millisecond)
|
||||||
|
if merr != nil {
|
||||||
|
resolve["models_error"] = merr.Error()
|
||||||
|
writeJSON(out, "resolve_quick.json", resolve)
|
||||||
|
failReport(out, "FAIL", "GET /models network: "+merr.Error(), resolve)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
resolve["models_http_status"] = resp.StatusCode
|
||||||
|
resolve["models_elapsed"] = elapsed.String()
|
||||||
|
resolve["models_snippet"] = trunc(string(body), 180)
|
||||||
|
if resp.StatusCode == http.StatusBadGateway {
|
||||||
|
writeJSON(out, "resolve_quick.json", resolve)
|
||||||
|
failReport(out, "FAIL", "FAIL 502: GET /v1/models — OverloadedBot unavailable (no mock, no wait)", resolve)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized {
|
||||||
|
writeJSON(out, "resolve_quick.json", resolve)
|
||||||
|
failReport(out, "FAIL", fmt.Sprintf("GET /models HTTP %d", resp.StatusCode), resolve)
|
||||||
|
}
|
||||||
|
// Prefer advertised green/code-fast if DB model is bare code-fast (gateway list).
|
||||||
|
if model == "code-fast" && strings.Contains(string(body), `"green/code-fast"`) {
|
||||||
|
client.Model = "green/code-fast"
|
||||||
|
model = client.Model
|
||||||
|
resolve["completer_model_adjusted"] = model
|
||||||
|
log.Printf("adjusted model code-fast -> green/code-fast from /models list")
|
||||||
|
}
|
||||||
|
// Cap per-call wait (~2.5m) and retries so we never sit on 4m×N.
|
||||||
|
if client.HTTPClient != nil {
|
||||||
|
client.HTTPClient.Timeout = 150 * time.Second
|
||||||
|
}
|
||||||
|
client.MaxRetries = 1
|
||||||
|
resolve["client_http_timeout"] = "150s"
|
||||||
|
resolve["client_max_retries"] = 1
|
||||||
|
writeJSON(out, "resolve_quick.json", resolve)
|
||||||
|
log.Printf("LIVE OK models=%d base=%s model=%s", resp.StatusCode, base, model)
|
||||||
|
|
||||||
|
pipeline := processing.NewPipeline(pool)
|
||||||
|
pipeline.BatchSize = cfg.ProcessingBatchSize
|
||||||
|
pipeline.AI = aiSvc
|
||||||
|
pipeline.Prompts = aiprompts.NewService(pool)
|
||||||
|
pipeline.Engine = &processing.Engine{
|
||||||
|
Completer: nil,
|
||||||
|
Vector: processing.NoopVectorCategorizer{},
|
||||||
|
EPREL: eprel.NewClient(eprel.Options{Enabled: true, Timeout: 15 * time.Second}),
|
||||||
|
ProviderMode: processing.AIProviderInternal,
|
||||||
|
}
|
||||||
|
|
||||||
|
rawIDs := make([]uuid.UUID, 0, len(eans))
|
||||||
|
for _, ean := range eans {
|
||||||
|
var id uuid.UUID
|
||||||
|
if err := pool.QueryRow(ctx, `SELECT id FROM raw_products WHERE company_id=$1 AND gtin=$2`, companyID, ean).Scan(&id); err != nil {
|
||||||
|
failReport(out, "FAIL", fmt.Sprintf("raw %s: %v", ean, err), resolve)
|
||||||
|
}
|
||||||
|
rawIDs = append(rawIDs, id)
|
||||||
|
}
|
||||||
|
_, _ = pool.Exec(ctx, `
|
||||||
|
UPDATE processed_products pp
|
||||||
|
SET field_sources = COALESCE(field_sources, '{}'::jsonb) - 'enhance_input_hash', updated_at=now()
|
||||||
|
FROM raw_products rp
|
||||||
|
WHERE pp.company_id=$1 AND pp.raw_product_id=rp.id AND rp.gtin=ANY($2::text[])`, companyID, eans)
|
||||||
|
_, _ = pool.Exec(ctx, `
|
||||||
|
UPDATE processing_jobs SET status='failed', error='yielded to quick final verify', updated_at=now()
|
||||||
|
WHERE company_id=$1 AND status IN ('pending','running','processing')`, companyID)
|
||||||
|
|
||||||
|
jobs, err := pipeline.StartJob(ctx, companyID, userID, rawIDs, "full")
|
||||||
|
if err != nil || len(jobs) == 0 {
|
||||||
|
failReport(out, "FAIL", fmt.Sprintf("StartJob: %v", err), resolve)
|
||||||
|
}
|
||||||
|
jobID := jobs[0].ID
|
||||||
|
var dbStatus string
|
||||||
|
var processedCount int
|
||||||
|
_ = pool.QueryRow(ctx, `SELECT lower(status), COALESCE(processed_count,0) FROM processing_jobs WHERE id=$1`, jobID).Scan(&dbStatus, &processedCount)
|
||||||
|
startOK := (dbStatus == "pending" || dbStatus == "queued") && processedCount == 0
|
||||||
|
startSample := map[string]any{
|
||||||
|
"process_id": jobID.String(), "status": dbStatus, "processed_items": processedCount,
|
||||||
|
"total_items": len(rawIDs), "start_ok": startOK,
|
||||||
|
}
|
||||||
|
writeJSON(out, "start_sample.json", startSample)
|
||||||
|
|
||||||
|
_, _ = pool.Exec(ctx, `UPDATE processing_jobs SET status='running', started_at=COALESCE(started_at,now()), updated_at=now() WHERE id=$1`, jobID)
|
||||||
|
|
||||||
|
// Hard wall: 2 products × ~2.5–3 min ≈ 8 min max
|
||||||
|
runCtx, runCancel := context.WithTimeout(ctx, 8*time.Minute)
|
||||||
|
defer runCancel()
|
||||||
|
procErr := pipeline.ProcessJob(runCtx, jobID)
|
||||||
|
logBytes, _ := os.ReadFile(logPath)
|
||||||
|
logText := string(logBytes)
|
||||||
|
aiEnhance := strings.Contains(logText, "ai_enhance")
|
||||||
|
|
||||||
|
items, loadErr := pipeline.LoadV1ProcessJobItems(ctx, companyID, jobID, "full")
|
||||||
|
if loadErr != nil {
|
||||||
|
items = nil
|
||||||
|
}
|
||||||
|
cards := []map[string]any{}
|
||||||
|
allPass := startOK && aiEnhance && procErr == nil
|
||||||
|
for _, item := range items {
|
||||||
|
ean := str(item["ean"])
|
||||||
|
if ean == "" {
|
||||||
|
ean = str(item["product_id"])
|
||||||
|
}
|
||||||
|
title := first(str(item["name"]), str(item["title"]))
|
||||||
|
desc := str(item["description"])
|
||||||
|
catName := str(item["category_name"])
|
||||||
|
if catName == "" {
|
||||||
|
if m, ok := item["category"].(map[string]any); ok {
|
||||||
|
catName = str(m["name"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, hasMetaT := item["meta_title"]
|
||||||
|
_, hasMetaD := item["meta_description"]
|
||||||
|
_, hasID := item["id"]
|
||||||
|
_, hasPP := item["processed_product_id"]
|
||||||
|
_, hasRaw := item["raw_product_id"]
|
||||||
|
hasHTML := strings.Contains(strings.ToLower(desc), "<h2") || strings.Contains(strings.ToLower(desc), "<p>")
|
||||||
|
checks := map[string]bool{
|
||||||
|
"name_present": strings.TrimSpace(title) != "",
|
||||||
|
"category_name_present": strings.TrimSpace(catName) != "",
|
||||||
|
"no_meta_title": !hasMetaT || item["meta_title"] == nil,
|
||||||
|
"no_meta_description": !hasMetaD || item["meta_description"] == nil,
|
||||||
|
"no_id": !hasID,
|
||||||
|
"no_processed_product_id": !hasPP,
|
||||||
|
"no_raw_product_id": !hasRaw,
|
||||||
|
"formula_html_or_empty": hasHTML || strings.TrimSpace(desc) == "" || strings.Contains(strings.ToLower(logText), "empty_or_provider_error") || strings.Contains(strings.ToLower(logText), "refuse"),
|
||||||
|
}
|
||||||
|
pass := true
|
||||||
|
for _, v := range checks {
|
||||||
|
if !v {
|
||||||
|
pass = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pass {
|
||||||
|
allPass = false
|
||||||
|
}
|
||||||
|
cards = append(cards, map[string]any{
|
||||||
|
"ean": ean, "title": title, "category": catName, "desc_len": len(desc),
|
||||||
|
"has_html": hasHTML, "checks": checks, "pass": pass,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if procErr != nil {
|
||||||
|
allPass = false
|
||||||
|
}
|
||||||
|
overall := "PASS"
|
||||||
|
reason := ""
|
||||||
|
if !allPass {
|
||||||
|
overall = "FAIL"
|
||||||
|
if procErr != nil {
|
||||||
|
reason = "ProcessJob: " + processing.TruncateError(procErr)
|
||||||
|
} else if !aiEnhance {
|
||||||
|
reason = "no ai_enhance log lines"
|
||||||
|
} else if !startOK {
|
||||||
|
reason = "start semantics not pending/0"
|
||||||
|
} else {
|
||||||
|
reason = "scorecard assertion failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report := map[string]any{
|
||||||
|
"generated_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"overall": overall,
|
||||||
|
"blocking_reason": reason,
|
||||||
|
"sync_pass": true,
|
||||||
|
"live_llm_pass": true,
|
||||||
|
"process_run": true,
|
||||||
|
"job_id": jobID.String(),
|
||||||
|
"eans": eans,
|
||||||
|
"start_ok": startOK,
|
||||||
|
"ai_enhance_seen": aiEnhance,
|
||||||
|
"process_error": nil,
|
||||||
|
"scorecards": cards,
|
||||||
|
"llm": resolve,
|
||||||
|
"sync": syncInfo,
|
||||||
|
"note": "slim verify: 2 products, 150s/call, 8m wall; repo seed only",
|
||||||
|
}
|
||||||
|
if procErr != nil {
|
||||||
|
report["process_error"] = processing.TruncateError(procErr)
|
||||||
|
}
|
||||||
|
writeJSON(out, "report.json", report)
|
||||||
|
writeMD(out, report, startSample, cards, logText)
|
||||||
|
fmt.Printf("OVERALL %s job=%s ai_enhance=%v err=%v\n", overall, jobID, aiEnhance, procErr)
|
||||||
|
if overall != "PASS" {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failReport(out, overall, reason string, extra map[string]any) {
|
||||||
|
report := map[string]any{
|
||||||
|
"generated_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"overall": overall, "blocking_reason": reason,
|
||||||
|
"process_run": false, "extra": extra,
|
||||||
|
}
|
||||||
|
writeJSON(out, "report.json", report)
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("# Final verify 2026-08-17 (quick LIVE)\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf("**Overall: %s**\n\n", overall))
|
||||||
|
b.WriteString(fmt.Sprintf("Blocking: %s\n\n", reason))
|
||||||
|
b.WriteString("Live OverloadedBot only. No mock.\n")
|
||||||
|
_ = os.WriteFile(filepath.Join(out, "REPORT.md"), []byte(b.String()), 0o644)
|
||||||
|
log.Print(reason)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMD(out string, report map[string]any, start map[string]any, cards []map[string]any, logText string) {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("# Final verify 2026-08-17 (quick LIVE)\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf("**Overall: %v**\n\n", report["overall"]))
|
||||||
|
if r := str(report["blocking_reason"]); r != "" {
|
||||||
|
b.WriteString(fmt.Sprintf("Blocking: %s\n\n", r))
|
||||||
|
}
|
||||||
|
b.WriteString("## Progress note\n\n")
|
||||||
|
b.WriteString("Prior hung run killed (4m chat waits on 6 EANs). This pass: live OverloadedBot only, repo seed sync, 2 products, 150s/call, 8m wall.\n\n")
|
||||||
|
b.WriteString("## 1. Sync A1 (repo seed only)\n\n")
|
||||||
|
b.WriteString("- Source: `scripts/seed/wp_product_categories.sql` (no upload, SkipDumpBackfill)\n")
|
||||||
|
b.WriteString("- See `sync_quick.json`\n\n")
|
||||||
|
b.WriteString("## 2. Live LLM\n\n")
|
||||||
|
if llm, ok := report["llm"].(map[string]any); ok {
|
||||||
|
b.WriteString(fmt.Sprintf("- base: `%v`\n- model: `%v`\n- models HTTP: `%v`\n- source: `%v`\n\n",
|
||||||
|
llm["completer_base"], llm["completer_model"], llm["models_http_status"], llm["role_source"]))
|
||||||
|
}
|
||||||
|
b.WriteString("## 3. Start semantics\n\n")
|
||||||
|
b.WriteString(fmt.Sprintf("- process_id: `%v`\n- status: `%v`\n- processed_items: `%v`\n- start_ok: `%v`\n\n",
|
||||||
|
start["process_id"], start["status"], start["processed_items"], start["start_ok"]))
|
||||||
|
b.WriteString(fmt.Sprintf("## 4. ai_enhance\n\n- seen: `%v`\n\n", report["ai_enhance_seen"]))
|
||||||
|
b.WriteString("## 5. Scorecards\n\n")
|
||||||
|
for _, c := range cards {
|
||||||
|
b.WriteString(fmt.Sprintf("- **%v** pass=%v cat=%v html=%v title=%q\n",
|
||||||
|
c["ean"], c["pass"], c["category"], c["has_html"], trunc(str(c["title"]), 60)))
|
||||||
|
}
|
||||||
|
b.WriteString("\n## Log excerpt (ai_enhance)\n\n```\n")
|
||||||
|
n := 0
|
||||||
|
for _, line := range strings.Split(logText, "\n") {
|
||||||
|
if strings.Contains(line, "ai_enhance") || strings.Contains(line, "LIVE") || strings.Contains(line, "OVERALL") {
|
||||||
|
b.WriteString(trunc(line, 240) + "\n")
|
||||||
|
n++
|
||||||
|
if n >= 12 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("```\n")
|
||||||
|
_ = os.WriteFile(filepath.Join(out, "REPORT.md"), []byte(b.String()), 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(out, name string, v any) {
|
||||||
|
raw, _ := json.MarshalIndent(v, "", " ")
|
||||||
|
_ = os.WriteFile(filepath.Join(out, name), raw, 0o644)
|
||||||
|
}
|
||||||
|
func str(v any) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
func first(vals ...string) string {
|
||||||
|
for _, v := range vals {
|
||||||
|
if strings.TrimSpace(v) != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func trunc(s string, n int) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:n] + "…"
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/db"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/platformsettings"
|
||||||
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
func main() {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "config: %v\n", err); os.Exit(2) }
|
||||||
|
ctx := context.Background()
|
||||||
|
pool, err := db.NewPool(ctx, cfg.DatabaseURL, db.PoolOptions{
|
||||||
|
MaxConns: int32(cfg.DBMaxConns), MinConns: int32(cfg.DBMinConns),
|
||||||
|
MaxConnLifetime: cfg.DBMaxConnLifetime, MaxConnLifetimeJitter: cfg.DBMaxConnLifetimeJitter,
|
||||||
|
MaxConnIdleTime: cfg.DBMaxConnIdleTime, HealthCheckPeriod: cfg.DBHealthCheckPeriod,
|
||||||
|
StatementTimeout: cfg.DBStatementTimeout,
|
||||||
|
})
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "db: %v\n", err); os.Exit(2) }
|
||||||
|
defer pool.Close()
|
||||||
|
platEnv := platformsettings.EnvConfig{AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey, TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL, OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel}
|
||||||
|
plat := platformsettings.NewService(pool, platEnv)
|
||||||
|
ai := aiprovider.NewService(pool, aiprovider.EnvConfig{AppEncryptionKey: cfg.AppEncryptionKey, CredentialsEncryptionKey: cfg.CredentialsEncryptionKey, TokenSigningSecret: cfg.TokenSigningSecret, DatabaseURL: cfg.DatabaseURL, OpenAIAPIKey: cfg.OpenAIAPIKey, OpenAIBaseURL: cfg.OpenAIBaseURL, OpenAIModel: cfg.OpenAIModel})
|
||||||
|
ai.Platform = plat
|
||||||
|
companyID := uuid.MustParse("604f23a8-b66e-4b21-8b45-0d72b68f4790")
|
||||||
|
roleCfg, err := plat.ResolveAIConfig(ctx, platformsettings.AIRoleProcessing)
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "ResolveAIConfig: %v\n", err); os.Exit(2) }
|
||||||
|
completer, mode, byok, err := ai.ResolveCompleterForRole(ctx, companyID, aiprovider.RoleProcessing)
|
||||||
|
if err != nil { fmt.Fprintf(os.Stderr, "ResolveCompleter: %v\n", err); os.Exit(2) }
|
||||||
|
client, ok := completer.(*processing.OpenAIClient)
|
||||||
|
if !ok { fmt.Fprintf(os.Stderr, "not OpenAIClient: %T\n", completer); os.Exit(2) }
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(client.BaseURL), "/")
|
||||||
|
modelsURL := base + "/models"
|
||||||
|
fmt.Printf("source=%s provider=%s base=%s model=%s mode=%s byok=%v key_len=%d\n", roleCfg.Source, roleCfg.Provider, base, client.Model, mode, byok, len(client.APIKey))
|
||||||
|
if processing.IsMockOrLoopbackBaseURL(base) { fmt.Println("FAIL mock/loopback"); os.Exit(3) }
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, modelsURL, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+client.APIKey)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
elapsed := time.Since(start).Round(time.Millisecond)
|
||||||
|
if err != nil { fmt.Printf("models_error=%v elapsed=%s\n", err, elapsed); os.Exit(4) }
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||||
|
resp.Body.Close()
|
||||||
|
snip := strings.TrimSpace(string(body))
|
||||||
|
if len(snip) > 160 { snip = snip[:160]+"…" }
|
||||||
|
fmt.Printf("models_http=%d elapsed=%s snippet=%q\n", resp.StatusCode, elapsed, snip)
|
||||||
|
if resp.StatusCode == 502 { os.Exit(5) }
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 401 { os.Exit(6) }
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Command repair-category-prompts rewrites A1 / Platform Demo categories.prompt
|
// Command repair-category-prompts rewrites A1 / Platform Demo categories.prompt
|
||||||
// values into aiprompts role-sectioned overlays (Title / Description / Meta /
|
// values into aiprompts role-sectioned overlays (Title / Description / Meta /
|
||||||
// Attributes). Prefers wp_product_categories.sql (SEED_A1_WP_CATEGORIES /
|
// Attributes). Prefers wp_product_categories.sql (SEED_A1_WP_CATEGORIES /
|
||||||
// Downloads) as source of truth, else a1-category-prompts.json. Legacy combined
|
// scripts/seed) as source of truth, else a1-category-prompts.json. Legacy combined
|
||||||
// Slovenian name+description blobs are split so naming rules land under Title
|
// Slovenian name+description blobs are split so naming rules land under Title
|
||||||
// and HTML under Description; otherwise CategoryEnhanceUserTemplate is used.
|
// and HTML under Description; otherwise CategoryEnhanceUserTemplate is used.
|
||||||
// Also repairs empty or brand-only title_template and empty description_template
|
// Also repairs empty or brand-only title_template and empty description_template
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
//
|
//
|
||||||
// go run ./cmd/repair-category-prompts -dry-run
|
// go run ./cmd/repair-category-prompts -dry-run
|
||||||
// go run ./cmd/repair-category-prompts -apply
|
// go run ./cmd/repair-category-prompts -apply
|
||||||
// go run ./cmd/repair-category-prompts -apply -wp-categories "D:/Users/.../Downloads/wp_product_categories.sql"
|
// go run ./cmd/repair-category-prompts -apply -wp-categories ../../scripts/seed/wp_product_categories.sql
|
||||||
// go run ./cmd/repair-category-prompts -apply -prompts ../../scripts/seed/a1-category-prompts.json
|
// go run ./cmd/repair-category-prompts -apply -prompts ../../scripts/seed/a1-category-prompts.json
|
||||||
//
|
//
|
||||||
// DATABASE_URL / -postgres required. Default is dry-run (count only).
|
// DATABASE_URL / -postgres required. Default is dry-run (count only).
|
||||||
|
|||||||
@@ -48,11 +48,11 @@ type RepairCategoryEnhancePromptsResult struct {
|
|||||||
// RepairA1DemoOptions configures optional seed overlay path for legacy splits.
|
// RepairA1DemoOptions configures optional seed overlay path for legacy splits.
|
||||||
type RepairA1DemoOptions struct {
|
type RepairA1DemoOptions struct {
|
||||||
// SeedPromptsPath points at a1-category-prompts.json OR wp_product_categories.sql.
|
// SeedPromptsPath points at a1-category-prompts.json OR wp_product_categories.sql.
|
||||||
// Empty → auto-detect WP SQL (SEED_A1_WP_CATEGORIES / Downloads), else JSON seed.
|
// Empty → auto-detect WP SQL (SEED_A1_WP_CATEGORIES / scripts/seed), else JSON seed.
|
||||||
SeedPromptsPath string
|
SeedPromptsPath string
|
||||||
// WPCategoriesPath forces wp_product_categories.sql (overrides SeedPromptsPath when set).
|
// WPCategoriesPath forces wp_product_categories.sql (overrides SeedPromptsPath when set).
|
||||||
WPCategoriesPath string
|
WPCategoriesPath string
|
||||||
// WPCategoriesSQL is uploaded dump bytes (primary for Admin Sync A1 in prod).
|
// WPCategoriesSQL is optional dump bytes (legacy Admin Sync A1 upload / API clients).
|
||||||
// When non-empty, takes precedence over filesystem auto-detect / path options.
|
// When non-empty, takes precedence over filesystem auto-detect / path options.
|
||||||
WPCategoriesSQL []byte
|
WPCategoriesSQL []byte
|
||||||
// ForceFromSeed overwrites already-sectioned prompts when a seed match exists
|
// ForceFromSeed overwrites already-sectioned prompts when a seed match exists
|
||||||
@@ -63,7 +63,7 @@ type RepairA1DemoOptions struct {
|
|||||||
// RepairA1DemoCategoryEnhancePrompts replaces non-sectioned / legacy combined
|
// RepairA1DemoCategoryEnhancePrompts replaces non-sectioned / legacy combined
|
||||||
// categories.prompt values for A1 Slovenija + Platform Demo with role-sectioned
|
// categories.prompt values for A1 Slovenija + Platform Demo with role-sectioned
|
||||||
// overlays (Title / Description / Meta / Attributes). Prefers wp_product_categories.sql
|
// overlays (Title / Description / Meta / Attributes). Prefers wp_product_categories.sql
|
||||||
// (SEED_A1_WP_CATEGORIES / Downloads) as source of truth, then a1-category-prompts.json,
|
// (SEED_A1_WP_CATEGORIES / scripts/seed) as source of truth, then a1-category-prompts.json,
|
||||||
// splitting combined Name+Description prompts so naming rules land under Title and
|
// splitting combined Name+Description prompts so naming rules land under Title and
|
||||||
// HTML body under Description; otherwise fall back to CategoryEnhanceUserTemplate.
|
// HTML body under Description; otherwise fall back to CategoryEnhanceUserTemplate.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ResolveWPCategoryPromptsPath picks an explicit path, else the first readable
|
// ResolveWPCategoryPromptsPath picks an explicit path, else the first readable
|
||||||
// wp_product_categories.sql candidate (SEED_A1_WP_CATEGORIES + Downloads + scripts/seed).
|
// wp_product_categories.sql candidate (SEED_A1_WP_CATEGORIES, then scripts/seed,
|
||||||
// Used by Sync A1 / RepairCompanyCategoryEnhancePrompts as the A1 prompt source of truth.
|
// then optional local Downloads as last-resort). Used by Sync A1 /
|
||||||
|
// RepairCompanyCategoryEnhancePrompts as the A1 prompt source of truth — no UI upload required.
|
||||||
func ResolveWPCategoryPromptsPath(explicit string) string {
|
func ResolveWPCategoryPromptsPath(explicit string) string {
|
||||||
if p := strings.TrimSpace(explicit); p != "" {
|
if p := strings.TrimSpace(explicit); p != "" {
|
||||||
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
if st, err := os.Stat(p); err == nil && !st.IsDir() {
|
||||||
@@ -31,7 +32,7 @@ func ResolveWPCategoryPromptsPath(explicit string) string {
|
|||||||
if st, err := os.Stat(v); err == nil && !st.IsDir() {
|
if st, err := os.Stat(v); err == nil && !st.IsDir() {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
log.Printf("warning: %s=%q not readable — trying Downloads / scripts/seed", envSeedA1WPCategories, v)
|
log.Printf("warning: %s=%q not readable — trying scripts/seed", envSeedA1WPCategories, v)
|
||||||
}
|
}
|
||||||
for _, c := range WPCategoryPromptsCandidates() {
|
for _, c := range WPCategoryPromptsCandidates() {
|
||||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||||
@@ -42,7 +43,8 @@ func ResolveWPCategoryPromptsPath(explicit string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WPCategoryPromptsCandidates lists local paths Sync A1 / repair try when
|
// WPCategoryPromptsCandidates lists local paths Sync A1 / repair try when
|
||||||
// SEED_A1_WP_CATEGORIES is unset. First readable file wins via ResolveWPCategoryPromptsPath.
|
// SEED_A1_WP_CATEGORIES is unset. Prefer committed scripts/seed first; Downloads
|
||||||
|
// is last-resort only. First readable file wins via ResolveWPCategoryPromptsPath.
|
||||||
func WPCategoryPromptsCandidates() []string {
|
func WPCategoryPromptsCandidates() []string {
|
||||||
var out []string
|
var out []string
|
||||||
if v := strings.TrimSpace(os.Getenv(envSeedA1WPCategories)); v != "" {
|
if v := strings.TrimSpace(os.Getenv(envSeedA1WPCategories)); v != "" {
|
||||||
@@ -53,13 +55,27 @@ func WPCategoryPromptsCandidates() []string {
|
|||||||
"wp_product_categories (1).sql",
|
"wp_product_categories (1).sql",
|
||||||
"wp_product_categories(1).sql",
|
"wp_product_categories(1).sql",
|
||||||
}
|
}
|
||||||
|
// Repo seed is the default source of truth (no upload / Downloads required).
|
||||||
|
if root, ok := findMonorepoRootFromCwd(); ok {
|
||||||
|
for _, n := range names {
|
||||||
|
out = append(out, filepath.Join(root, "scripts", "seed", n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, n := range names {
|
||||||
|
out = append(out,
|
||||||
|
filepath.Join("scripts", "seed", n),
|
||||||
|
filepath.Join("..", "..", "scripts", "seed", n),
|
||||||
|
n,
|
||||||
|
filepath.Join("..", "..", n),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Optional local Downloads fallback (legacy / one-off machines without seed).
|
||||||
home, _ := os.UserHomeDir()
|
home, _ := os.UserHomeDir()
|
||||||
if home != "" {
|
if home != "" {
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
out = append(out, filepath.Join(home, "Downloads", n))
|
out = append(out, filepath.Join(home, "Downloads", n))
|
||||||
out = append(out, filepath.Join(home, "downloads", n))
|
out = append(out, filepath.Join(home, "downloads", n))
|
||||||
}
|
}
|
||||||
// Windows secondary profile Downloads (e.g. D:\Users\…\Downloads).
|
|
||||||
for _, driveRoot := range []string{`D:\`, `C:\`} {
|
for _, driveRoot := range []string{`D:\`, `C:\`} {
|
||||||
alt := filepath.Join(driveRoot, "Users", filepath.Base(home), "Downloads")
|
alt := filepath.Join(driveRoot, "Users", filepath.Base(home), "Downloads")
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
@@ -67,19 +83,6 @@ func WPCategoryPromptsCandidates() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, n := range names {
|
|
||||||
out = append(out,
|
|
||||||
n,
|
|
||||||
filepath.Join("..", "..", n),
|
|
||||||
filepath.Join("scripts", "seed", n),
|
|
||||||
filepath.Join("..", "..", "scripts", "seed", n),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if root, ok := findMonorepoRootFromCwd(); ok {
|
|
||||||
for _, n := range names {
|
|
||||||
out = append(out, filepath.Join(root, "scripts", "seed", n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,14 +81,10 @@ func TestParseWPProductCategoriesSQL_SlusalkeSplit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseWPProductCategoriesSQL_RealDownloadsFile(t *testing.T) {
|
func TestParseWPProductCategoriesSQL_RealSeedFile(t *testing.T) {
|
||||||
path := ResolveWPCategoryPromptsPath(`d:\Users\Green Eclipse\Downloads\wp_product_categories.sql`)
|
path := ResolveWPCategoryPromptsPath("")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
// Also try env / auto-detect without failing CI machines that lack the dump.
|
t.Skip("wp_product_categories.sql not available (expected under scripts/seed)")
|
||||||
path = ResolveWPCategoryPromptsPath("")
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
t.Skip("wp_product_categories.sql not available on this machine")
|
|
||||||
}
|
}
|
||||||
byNorm, _, err := loadWPCategoryPromptOverlays(path)
|
byNorm, _, err := loadWPCategoryPromptOverlays(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -109,6 +105,10 @@ func TestParseWPProductCategoriesSQL_RealDownloadsFile(t *testing.T) {
|
|||||||
if !strings.Contains(split, "tip izdelka lowercase") && !strings.Contains(split, "znamka") {
|
if !strings.Contains(split, "tip izdelka lowercase") && !strings.Contains(split, "znamka") {
|
||||||
t.Fatalf("unexpected Title rules in split: %s", split[:min(400, len(split))])
|
t.Fatalf("unexpected Title rules in split: %s", split[:min(400, len(split))])
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(filepath.ToSlash(path), "scripts/seed/wp_product_categories.sql") &&
|
||||||
|
filepath.Base(path) != "wp_product_categories.sql" {
|
||||||
|
t.Logf("resolved path %s (prefer scripts/seed when present)", path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveWPCategoryPromptsPath_Explicit(t *testing.T) {
|
func TestResolveWPCategoryPromptsPath_Explicit(t *testing.T) {
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ func IsWeakPriorEnhanceDescription(priorDesc string, titles ...string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// heuristicSynthesizePhrases are distinctive invent / formula-skeleton snippets from
|
// heuristicSynthesizePhrases are distinctive invent / formula-skeleton snippets from
|
||||||
// synthesizeDescriptionFromTitle / synthesizeDescriptionFromFormula. These may look
|
// synthesizeDescriptionFromTitle / synthesizeDescriptionFromFormula / factualDescriptionIntro.
|
||||||
// "strong" enough to pass IsWeakPriorEnhanceDescription but must never hash-skip
|
// These may look "strong" enough to pass IsWeakPriorEnhanceDescription but must never
|
||||||
// enhance (would leave fallback copy forever on reprocess).
|
// hash-skip enhance (would leave fallback copy forever on reprocess).
|
||||||
var heuristicSynthesizePhrases = []string{
|
var heuristicSynthesizePhrases = []string{
|
||||||
"is a catalog product with the known attributes",
|
"is a catalog product with the known attributes",
|
||||||
"is listed in the ",
|
"is listed in the ",
|
||||||
@@ -43,23 +43,63 @@ var heuristicSynthesizePhrases = []string{
|
|||||||
"je izdelek v kategoriji",
|
"je izdelek v kategoriji",
|
||||||
"je izdelek znamke",
|
"je izdelek znamke",
|
||||||
". ključne specifikacije:",
|
". ključne specifikacije:",
|
||||||
|
// factualDescriptionIntro (post-phrase-avoidance invent)
|
||||||
|
" — znamka ",
|
||||||
|
", znamka ",
|
||||||
|
" — katalogski izdelek",
|
||||||
|
" — from ",
|
||||||
|
" — catalog product",
|
||||||
}
|
}
|
||||||
|
|
||||||
// LooksLikeHeuristicSynthesize reports invent / formula-skeleton fallback copy.
|
// LooksLikeHeuristicSynthesize reports invent / formula-skeleton fallback copy.
|
||||||
func LooksLikeHeuristicSynthesize(desc string) bool {
|
func LooksLikeHeuristicSynthesize(desc string) bool {
|
||||||
lower := strings.ToLower(desc)
|
plain := strings.ToLower(plainTextForWeakCheck(desc))
|
||||||
|
if plain == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
for _, p := range heuristicSynthesizePhrases {
|
for _, p := range heuristicSynthesizePhrases {
|
||||||
if p != "" && strings.Contains(lower, p) {
|
if p != "" && strings.Contains(plain, p) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// EN invent: "<title> is a <category> product from <brand>"
|
// EN invent: "<title> is a <category> product from <brand>"
|
||||||
if strings.Contains(lower, " is a ") && strings.Contains(lower, " product from ") {
|
if strings.Contains(plain, " is a ") && strings.Contains(plain, " product from ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// SL/CS short invent: "<title> je … znamk|kategor|produkt …"
|
||||||
|
if strings.Contains(plain, " je ") &&
|
||||||
|
(strings.Contains(plain, "znamk") ||
|
||||||
|
strings.Contains(plain, "kategor") ||
|
||||||
|
strings.Contains(plain, "produkt") ||
|
||||||
|
strings.Contains(plain, "televiz") ||
|
||||||
|
strings.Contains(plain, "monitor")) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plainTextForWeakCheck strips HTML tags so <p>thin invent</p> is judged on visible copy.
|
||||||
|
func plainTextForWeakCheck(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s))
|
||||||
|
inTag := false
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case r == '<':
|
||||||
|
inTag = true
|
||||||
|
case r == '>':
|
||||||
|
inTag = false
|
||||||
|
case !inTag:
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
// EnhanceHashSkipBlockReason returns a stable reason when prior description must
|
// EnhanceHashSkipBlockReason returns a stable reason when prior description must
|
||||||
// not hash-skip the enhance LLM: "weak", "title-echo", "synth", or "".
|
// not hash-skip the enhance LLM: "weak", "title-echo", "synth", or "".
|
||||||
func EnhanceHashSkipBlockReason(priorDesc string, titles ...string) string {
|
func EnhanceHashSkipBlockReason(priorDesc string, titles ...string) string {
|
||||||
@@ -67,22 +107,26 @@ func EnhanceHashSkipBlockReason(priorDesc string, titles ...string) string {
|
|||||||
if priorDesc == "" || priorDesc == "<nil>" {
|
if priorDesc == "" || priorDesc == "<nil>" {
|
||||||
return "weak"
|
return "weak"
|
||||||
}
|
}
|
||||||
if len([]rune(priorDesc)) < minUsableProductDescRunes {
|
plain := plainTextForWeakCheck(priorDesc)
|
||||||
|
if plain == "" || plain == "<nil>" {
|
||||||
|
return "weak"
|
||||||
|
}
|
||||||
|
if len([]rune(plain)) < minUsableProductDescRunes {
|
||||||
return "weak"
|
return "weak"
|
||||||
}
|
}
|
||||||
for _, title := range titles {
|
for _, title := range titles {
|
||||||
if DescriptionEchoesTitle(priorDesc, title) {
|
if DescriptionEchoesTitle(plain, title) || DescriptionEchoesTitle(priorDesc, title) {
|
||||||
return "title-echo"
|
return "title-echo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ContainsWeakFillerPhrase(priorDesc) {
|
if ContainsWeakFillerPhrase(plain) || ContainsWeakFillerPhrase(priorDesc) {
|
||||||
return "weak"
|
return "weak"
|
||||||
}
|
}
|
||||||
if LooksLikeHeuristicSynthesize(priorDesc) {
|
if LooksLikeHeuristicSynthesize(priorDesc) {
|
||||||
return "synth"
|
return "synth"
|
||||||
}
|
}
|
||||||
if len([]rune(priorDesc)) <= shortBoilerplateDescRunes &&
|
if len([]rune(plain)) <= shortBoilerplateDescRunes &&
|
||||||
!descriptionOverlapsProductFacts(priorDesc, titles...) {
|
!descriptionOverlapsProductFacts(plain, titles...) {
|
||||||
return "weak"
|
return "weak"
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ func TestLooksLikeHeuristicSynthesize(t *testing.T) {
|
|||||||
if !LooksLikeHeuristicSynthesize(sl) {
|
if !LooksLikeHeuristicSynthesize(sl) {
|
||||||
t.Fatalf("expected SL invent detected: %q", sl)
|
t.Fatalf("expected SL invent detected: %q", sl)
|
||||||
}
|
}
|
||||||
|
htmlInvent := "<p>QLED TV 55 je televizor znamke Samsung.</p>"
|
||||||
|
if !LooksLikeHeuristicSynthesize(htmlInvent) {
|
||||||
|
t.Fatalf("expected HTML-wrapped invent detected: %q", htmlInvent)
|
||||||
|
}
|
||||||
|
if !ShouldRefuseEnhanceHashSkip(htmlInvent, "QLED TV 55") {
|
||||||
|
t.Fatalf("HTML invent must block hash skip")
|
||||||
|
}
|
||||||
|
factual := "QLED TV 55 — Televizorji, znamka Samsung."
|
||||||
|
if !LooksLikeHeuristicSynthesize(factual) {
|
||||||
|
t.Fatalf("expected factualDescriptionIntro invent detected: %q", factual)
|
||||||
|
}
|
||||||
good := "Vogel's WALL 3245 is a sturdy TV mount with clear load ratings and install guidance for wall displays."
|
good := "Vogel's WALL 3245 is a sturdy TV mount with clear load ratings and install guidance for wall displays."
|
||||||
if LooksLikeHeuristicSynthesize(good) {
|
if LooksLikeHeuristicSynthesize(good) {
|
||||||
t.Fatalf("retail copy must not look like invent: %q", good)
|
t.Fatalf("retail copy must not look like invent: %q", good)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TestMemberForbiddenOnSensitiveMutations(t *testing.T) {
|
|||||||
body string
|
body string
|
||||||
}{
|
}{
|
||||||
{name: "create_api_key", fn: s.handleCreateAPIKey, body: `{"name":"x"}`},
|
{name: "create_api_key", fn: s.handleCreateAPIKey, body: `{"name":"x"}`},
|
||||||
|
{name: "update_api_key", fn: s.handleUpdateAPIKey, body: `{"name":"renamed"}`},
|
||||||
{name: "revoke_api_key", fn: s.handleRevokeAPIKey, body: ""},
|
{name: "revoke_api_key", fn: s.handleRevokeAPIKey, body: ""},
|
||||||
{name: "put_email", fn: s.handlePutEmailIntegration, body: `{}`},
|
{name: "put_email", fn: s.handlePutEmailIntegration, body: `{}`},
|
||||||
{name: "verify_email", fn: s.handleVerifyEmailIntegration, body: ""},
|
{name: "verify_email", fn: s.handleVerifyEmailIntegration, body: ""},
|
||||||
|
|||||||
@@ -29,11 +29,13 @@ const syncA1MaxBodyBytes = catalog.MaxWPCategorySQLBytes + (2 << 20)
|
|||||||
//
|
//
|
||||||
// Body (JSON): confirm=true required; backfill_categories (default true);
|
// Body (JSON): confirm=true required; backfill_categories (default true);
|
||||||
// reprocess_sample_limit (default 25, max 200); mysql_dump optional path;
|
// reprocess_sample_limit (default 25, max 200); mysql_dump optional path;
|
||||||
// skip_dump_backfill (default false); wp_product_categories_sql_b64 optional
|
// skip_dump_backfill (default false). Category prompts load from repo seed
|
||||||
// base64 of wp_product_categories.sql (primary for category prompts).
|
// (scripts/seed/wp_product_categories.sql) via ResolveWPCategoryPromptsPath;
|
||||||
|
// wp_product_categories_sql_b64 remains accepted but unused by the admin UI.
|
||||||
//
|
//
|
||||||
// Body (multipart/form-data): same fields as form values; file field
|
// Body (multipart/form-data): same fields as form values; optional file field
|
||||||
// wp_product_categories or wp_categories_sql for the SQL dump upload.
|
// wp_product_categories / wp_categories_sql is still accepted for API clients
|
||||||
|
// but the admin Sync A1 UI no longer uploads.
|
||||||
//
|
//
|
||||||
// Flash (UI): result → flash.admin.syncA1Success.
|
// Flash (UI): result → flash.admin.syncA1Success.
|
||||||
func (s *Server) handleAdminSyncCompanyA1(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAdminSyncCompanyA1(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -116,8 +118,10 @@ func (s *Server) handleAdminSyncCompanyA1(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
if len(parsed.WPCategoriesSQL) > 0 {
|
if len(parsed.WPCategoriesSQL) > 0 {
|
||||||
note = "Applied uploaded wp_product_categories.sql (Title/Description/Meta/Attributes sections) + catalog hygiene. Reprocess recommended products manually (no mass reprocess)."
|
note = "Applied uploaded wp_product_categories.sql (Title/Description/Meta/Attributes sections) + catalog hygiene. Reprocess recommended products manually (no mass reprocess)."
|
||||||
} else if strings.TrimSpace(result.WPCategoriesPath) == "" {
|
} else if strings.TrimSpace(result.WPCategoriesPath) != "" {
|
||||||
note += " Upload wp_product_categories.sql on Sync A1 for force-applied category prompts when the dump is not on the API host."
|
note = "Applied category prompts from repo seed (" + result.WPCategoriesPath + ") + catalog hygiene. Reprocess recommended products manually (no mass reprocess)."
|
||||||
|
} else {
|
||||||
|
note += " Place scripts/seed/wp_product_categories.sql on the API host (or set SEED_A1_WP_CATEGORIES) for force-applied category prompts."
|
||||||
}
|
}
|
||||||
|
|
||||||
JSON(w, http.StatusOK, map[string]any{
|
JSON(w, http.StatusOK, map[string]any{
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -80,6 +83,61 @@ func (s *Server) handleCreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cid, _ := CompanyIDFromContext(r.Context())
|
||||||
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !s.requireFeatures(w, r, "settings.api_keys", "capability.api_access") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, "invalid id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := DecodeJSON(r, &body); err != nil {
|
||||||
|
Error(w, http.StatusBadRequest, "invalid json")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(body.Name)
|
||||||
|
if name == "" {
|
||||||
|
Error(w, http.StatusBadRequest, "name required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(name) > 120 {
|
||||||
|
Error(w, http.StatusBadRequest, "name too long")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
outID uuid.UUID
|
||||||
|
outName string
|
||||||
|
prefix string
|
||||||
|
lastUsed any
|
||||||
|
created any
|
||||||
|
)
|
||||||
|
err = s.Pool.QueryRow(r.Context(), `
|
||||||
|
UPDATE api_keys
|
||||||
|
SET name = $3, updated_at = now()
|
||||||
|
WHERE id = $1 AND company_id = $2 AND revoked_at IS NULL
|
||||||
|
RETURNING id, name, key_prefix, last_used_at, created_at`,
|
||||||
|
id, cid, name).Scan(&outID, &outName, &prefix, &lastUsed, &created)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
Error(w, http.StatusNotFound, "not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
Error(w, http.StatusInternalServerError, "update failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSON(w, http.StatusOK, map[string]any{
|
||||||
|
"id": outID, "name": outName, "key_prefix": prefix, "last_used_at": lastUsed, "created_at": created,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleRevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||||
cid, _ := CompanyIDFromContext(r.Context())
|
cid, _ := CompanyIDFromContext(r.Context())
|
||||||
if !s.allowCompanyAdminOrPlatform(w, r) {
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandleUpdateAPIKeyRejectsEmptyName(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := &Server{}
|
||||||
|
cid := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
||||||
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||||
|
keyID := uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
||||||
|
ctx = context.WithValue(ctx, ctxCompanyID, cid)
|
||||||
|
ctx = context.WithValue(ctx, ctxRole, "admin")
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("id", keyID.String())
|
||||||
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPatch, "/api/api-keys/"+keyID.String(), bytes.NewBufferString(`{"name":" "}`)).WithContext(ctx)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
s.handleUpdateAPIKey(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d body=%s, want 400", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandleUpdateAPIKeyRejectsInvalidID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := &Server{}
|
||||||
|
cid := uuid.MustParse("11111111-1111-1111-1111-111111111111")
|
||||||
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), ctxUserID, uid)
|
||||||
|
ctx = context.WithValue(ctx, ctxCompanyID, cid)
|
||||||
|
ctx = context.WithValue(ctx, ctxRole, "admin")
|
||||||
|
|
||||||
|
rctx := chi.NewRouteContext()
|
||||||
|
rctx.URLParams.Add("id", "not-a-uuid")
|
||||||
|
ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPatch, "/api/api-keys/not-a-uuid", bytes.NewBufferString(`{"name":"ok"}`)).WithContext(ctx)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
s.handleUpdateAPIKey(rec, req)
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d body=%s, want 400", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -247,6 +247,15 @@ func TestAuthSessionCoreEndpoints(t *testing.T) {
|
|||||||
t.Fatalf("create api-key payload=%v", created)
|
t.Fatalf("create api-key payload=%v", created)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rec = do(http.MethodPatch, "/api/api-keys/"+keyID, `{"name":"auth-smoke-key-renamed"}`, true)
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("rename api-key status=%d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
renamed := decode(t, rec)
|
||||||
|
if fmt.Sprint(renamed["name"]) != "auth-smoke-key-renamed" {
|
||||||
|
t.Fatalf("rename api-key name=%v", renamed["name"])
|
||||||
|
}
|
||||||
|
|
||||||
// Public v1 with the new key (CSRF skipped).
|
// Public v1 with the new key (CSRF skipped).
|
||||||
v1 := httptest.NewRecorder()
|
v1 := httptest.NewRecorder()
|
||||||
v1Req := httptest.NewRequest(http.MethodGet, "/api/v1/products?limit=1", nil)
|
v1Req := httptest.NewRequest(http.MethodGet, "/api/v1/products?limit=1", nil)
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ func (s *Server) Router() http.Handler {
|
|||||||
|
|
||||||
r.Get("/api-keys", s.handleListAPIKeys)
|
r.Get("/api-keys", s.handleListAPIKeys)
|
||||||
r.Post("/api-keys", s.handleCreateAPIKey)
|
r.Post("/api-keys", s.handleCreateAPIKey)
|
||||||
|
r.Patch("/api-keys/{id}", s.handleUpdateAPIKey)
|
||||||
r.Delete("/api-keys/{id}", s.handleRevokeAPIKey)
|
r.Delete("/api-keys/{id}", s.handleRevokeAPIKey)
|
||||||
|
|
||||||
r.Get("/billing/credits", s.handleCreditsOverview)
|
r.Get("/billing/credits", s.handleCreditsOverview)
|
||||||
|
|||||||
@@ -118,8 +118,9 @@ info:
|
|||||||
- GET /products data[].id = processed_products.id (enriched row)
|
- GET /products data[].id = processed_products.id (enriched row)
|
||||||
- GET /products data[].raw_product_id = raw_products.id (use this for raw_product_ids)
|
- GET /products data[].raw_product_id = raw_products.id (use this for raw_product_ids)
|
||||||
- POST ... raw_product_ids[] must be raw_products.id — never PresentProduct.id
|
- POST ... raw_product_ids[] must be raw_products.id — never PresentProduct.id
|
||||||
- GET /products/process/{id} COMPLETED items[].id = processed_products.id (legacy);
|
- GET /products/process/{id} COMPLETED items[] omit internal UUIDs (id /
|
||||||
additive processed_product_id (same as id) and raw_product_id (raw_products.id)
|
processed_product_id / raw_product_id). Use GET /products when a UUID is needed.
|
||||||
|
Display name is items[].name (title omitted when identical).
|
||||||
|
|
||||||
Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
|
Note: Dashboard JSON under /api/* uses session cookies + CSRF and is separate
|
||||||
from this public API-key surface. Other legacy path aliases
|
from this public API-key surface. Other legacy path aliases
|
||||||
@@ -895,9 +896,10 @@ paths:
|
|||||||
Completed jobs return items[] (EAN-keyed enrichment). In-progress and failed
|
Completed jobs return items[] (EAN-keyed enrichment). In-progress and failed
|
||||||
jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).
|
jobs omit items. Not the same shape as GET /process/{id} (flat ProcessingJob).
|
||||||
|
|
||||||
On COMPLETED items, id is the processed_products UUID (legacy). Additive aliases:
|
On COMPLETED items, name is the product display name (title is omitted when
|
||||||
processed_product_id (same value as id), raw_product_id (raw_products.id), and
|
identical). Internal UUIDs (id / processed_product_id / raw_product_id) are
|
||||||
name (same value as title) for dual-mode clients / scorecards.
|
omitted — use GET /products for those. A1 cohort / Platform Demo / A1-prompt
|
||||||
|
companies omit meta_title and meta_description even if stored.
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -925,17 +927,11 @@ paths:
|
|||||||
processing_type: full
|
processing_type: full
|
||||||
items:
|
items:
|
||||||
- ean: '8606019604493'
|
- ean: '8606019604493'
|
||||||
id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
|
||||||
processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
|
||||||
raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc
|
|
||||||
status: processed
|
status: processed
|
||||||
category: Cookers
|
category: Cookers
|
||||||
category_id: '50'
|
category_id: '50'
|
||||||
category_name: Cookers
|
category_name: Cookers
|
||||||
title: VOX electric cooker EHT 6020 WG
|
|
||||||
name: VOX electric cooker EHT 6020 WG
|
name: VOX electric cooker EHT 6020 WG
|
||||||
meta_title: VOX electric cooker EHT 6020 WG | 50
|
|
||||||
meta_description: Affordable electric cooker with four plates and a 65 L fan oven.
|
|
||||||
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven."
|
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven."
|
||||||
attributes:
|
attributes:
|
||||||
brand: Vox
|
brand: Vox
|
||||||
@@ -6207,12 +6203,24 @@ components:
|
|||||||
process_id:
|
process_id:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Job lifecycle status on enqueue. Always pending (or processing if the
|
||||||
|
worker already picked it up). Never COMPLETED on start — poll GET
|
||||||
|
/products/process/{id} for completion.
|
||||||
|
example: pending
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
total_items:
|
total_items:
|
||||||
type: integer
|
type: integer
|
||||||
|
description: Number of products accepted into the job (enqueue size).
|
||||||
processed_items:
|
processed_items:
|
||||||
type: integer
|
type: integer
|
||||||
|
description: |
|
||||||
|
Count of items that finished processing. Always 0 on start while the
|
||||||
|
job is pending/processing; increments as the worker completes products.
|
||||||
|
example: 0
|
||||||
job_count:
|
job_count:
|
||||||
type: integer
|
type: integer
|
||||||
description: Present when StartJob auto-splits
|
description: Present when StartJob auto-splits
|
||||||
@@ -6282,35 +6290,19 @@ components:
|
|||||||
expose category as the human-readable display name (category_id holds
|
expose category as the human-readable display name (category_id holds
|
||||||
categories.unique_id), a description string that may include category formula
|
categories.unique_id), a description string that may include category formula
|
||||||
HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title /
|
HTML (h1/h2/h3/h4, p, ul — never a JSON array), optional SEO meta_title /
|
||||||
meta_description (plain text; omitted for A1 cohort), optional eprel object or
|
meta_description (plain text; omitted for A1 cohort, Platform Demo, and any
|
||||||
null, clean attributes, images, and dual-mode ids.
|
company with A1-style category role-section prompts — even if stored in DB),
|
||||||
Product display name is title; additive name mirrors the same processed title
|
optional eprel object or null, clean attributes, and images.
|
||||||
(dual-mode for scorecards / legacy clients that read name).
|
Product display name is name (primary). title is omitted when identical to name.
|
||||||
Property order prefers human-readable fields first (ean, title, category,
|
Internal UUIDs (id, processed_product_id, raw_product_id) are omitted from this
|
||||||
description, attributes, images, eprel) with internal ids last.
|
public shape — use catalog APIs when a product UUID is required.
|
||||||
|
Property order prefers human-readable fields first (ean, name, category,
|
||||||
|
description, attributes, images, eprel).
|
||||||
required:
|
required:
|
||||||
- ean
|
- ean
|
||||||
properties:
|
properties:
|
||||||
ean:
|
ean:
|
||||||
type: string
|
type: string
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: |
|
|
||||||
Legacy field: processed_products.id when enrichment succeeded.
|
|
||||||
Do not treat as raw_products.id. Same value as processed_product_id.
|
|
||||||
processed_product_id:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: |
|
|
||||||
Explicit alias of id (processed_products.id). Prefer this name in new
|
|
||||||
dual-mode clients; id remains for backward compatibility.
|
|
||||||
raw_product_id:
|
|
||||||
type: string
|
|
||||||
format: uuid
|
|
||||||
description: |
|
|
||||||
raw_products.id for this job line. Use with POST /process raw_product_ids
|
|
||||||
or dashboard catalog APIs. Present whenever the job product row exists.
|
|
||||||
category:
|
category:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -6330,32 +6322,32 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
Human-readable category display name (mirrors category when both are set).
|
Human-readable category display name (mirrors category when both are set).
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
nullable: true
|
|
||||||
description: |
|
|
||||||
Product display name (processed title). Primary legacy field; same value
|
|
||||||
as name when present.
|
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
Additive alias of title (same processed display name). Prefer title in
|
Product display name (processed title). Primary field for clients.
|
||||||
new clients; name remains for scorecards and legacy readers.
|
title:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Optional legacy alias of name. Omitted when identical to name.
|
||||||
meta_title:
|
meta_title:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
SEO title. Filled from processing meta or synthesized from title / category
|
SEO title. Filled from processing meta or synthesized from title / category
|
||||||
when empty so successful items are not left with null meta.
|
when empty so successful items are not left with null meta.
|
||||||
Omitted for A1 cohort (SEO meta is not used).
|
Omitted for A1 cohort, Platform Demo, and companies with A1-style category
|
||||||
|
prompts (SEO meta is not used).
|
||||||
meta_description:
|
meta_description:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
description: |
|
description: |
|
||||||
SEO description (word-safe truncate). Distinct from body description when
|
SEO description (word-safe truncate). Distinct from body description when
|
||||||
possible; synthesized from plain description when DB meta is empty.
|
possible; synthesized from plain description when DB meta is empty.
|
||||||
Omitted for A1 cohort (SEO meta is not used).
|
Omitted for A1 cohort, Platform Demo, and companies with A1-style category
|
||||||
|
prompts (SEO meta is not used).
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -6477,17 +6469,11 @@ components:
|
|||||||
processing_type: full
|
processing_type: full
|
||||||
items:
|
items:
|
||||||
- ean: '8606019604493'
|
- ean: '8606019604493'
|
||||||
id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
|
||||||
processed_product_id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
|
|
||||||
raw_product_id: cccccccc-cccc-cccc-cccc-cccccccccccc
|
|
||||||
status: processed
|
status: processed
|
||||||
category: Cookers
|
category: Cookers
|
||||||
category_id: '50'
|
category_id: '50'
|
||||||
category_name: Cookers
|
category_name: Cookers
|
||||||
title: VOX electric cooker EHT 6020 WG
|
|
||||||
name: VOX electric cooker EHT 6020 WG
|
name: VOX electric cooker EHT 6020 WG
|
||||||
meta_title: VOX electric cooker EHT 6020 WG | 50
|
|
||||||
meta_description: Affordable electric cooker with four plates and a 65 L fan oven.
|
|
||||||
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity."
|
description: "Vox Electronics electric cooker EHT 6020 WG offers strong value with four electric hobs and a 65 L fan oven. Energy class A with practical everyday capacity."
|
||||||
attributes:
|
attributes:
|
||||||
brand: Vox
|
brand: Vox
|
||||||
|
|||||||
@@ -160,11 +160,14 @@ func (s *Server) handleV1StartProcess(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
primary := jobs[0]
|
primary := jobs[0]
|
||||||
|
// processed_items is the completed count — stay 0 until the worker finishes.
|
||||||
|
// total_items is the enqueue size; status stays pending/processing until poll COMPLETED.
|
||||||
resp := map[string]any{
|
resp := map[string]any{
|
||||||
"process_id": primary.ID.String(),
|
"process_id": primary.ID.String(),
|
||||||
|
"status": "pending",
|
||||||
"message": fmt.Sprintf("Processing started for %d product(s)", len(rawIDs)),
|
"message": fmt.Sprintf("Processing started for %d product(s)", len(rawIDs)),
|
||||||
"total_items": totalItems,
|
"total_items": totalItems,
|
||||||
"processed_items": len(rawIDs),
|
"processed_items": 0,
|
||||||
}
|
}
|
||||||
if len(jobs) > 1 {
|
if len(jobs) > 1 {
|
||||||
siblings := make([]string, 0, len(jobs)-1)
|
siblings := make([]string, 0, len(jobs)-1)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ func TestHandleV1StartProcessItemsEANReturnsProcessID(t *testing.T) {
|
|||||||
var envelope struct {
|
var envelope struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
ProcessID string `json:"process_id"`
|
ProcessID string `json:"process_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
TotalItems int `json:"total_items"`
|
TotalItems int `json:"total_items"`
|
||||||
ProcessedItems int `json:"processed_items"`
|
ProcessedItems int `json:"processed_items"`
|
||||||
@@ -205,8 +206,11 @@ func TestHandleV1StartProcessItemsEANReturnsProcessID(t *testing.T) {
|
|||||||
if envelope.Data.ProcessID != jobID.String() {
|
if envelope.Data.ProcessID != jobID.String() {
|
||||||
t.Fatalf("process_id=%q want %s", envelope.Data.ProcessID, jobID)
|
t.Fatalf("process_id=%q want %s", envelope.Data.ProcessID, jobID)
|
||||||
}
|
}
|
||||||
if envelope.Data.TotalItems != 1 || envelope.Data.ProcessedItems != 1 {
|
if envelope.Data.Status != "pending" {
|
||||||
t.Fatalf("counts=%+v", envelope.Data)
|
t.Fatalf("status=%q want pending", envelope.Data.Status)
|
||||||
|
}
|
||||||
|
if envelope.Data.TotalItems != 1 || envelope.Data.ProcessedItems != 0 {
|
||||||
|
t.Fatalf("counts=%+v (processed_items must be 0 on start)", envelope.Data)
|
||||||
}
|
}
|
||||||
if enqueued != jobID {
|
if enqueued != jobID {
|
||||||
t.Fatalf("enqueued=%s", enqueued)
|
t.Fatalf("enqueued=%s", enqueued)
|
||||||
|
|||||||
@@ -528,11 +528,10 @@ func (e *Engine) RunSteps(ctx context.Context, companyID string, in ProductInput
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipelines without StepCategorize (enhance_only / normalize_only) still try
|
// Pipelines without StepCategorize (enhance_only / normalize_only) still run
|
||||||
// vector categorize when AllowAI + embeddings are available. Full/categorize
|
// vector + LLM categorize when category is empty so enhance is not fed a blank.
|
||||||
// already ran runCategorizeStep (vector then LLM) inside the loop.
|
|
||||||
if !stepsContain(steps, StepCategorize) {
|
if !stepsContain(steps, StepCategorize) {
|
||||||
tryVectorCategorize(ctx, e, companyID, &out, categoryNames, policy)
|
runCategorizeStep(ctx, e, companyID, &out, in, categoryNames, policy)
|
||||||
noteMissingCategory(&out, policy, e != nil && e.Vector != nil && e.Vector.Enabled())
|
noteMissingCategory(&out, policy, e != nil && e.Vector != nil && e.Vector.Enabled())
|
||||||
}
|
}
|
||||||
preserveCategoryIfEmpty(&out, in.PriorCategory)
|
preserveCategoryIfEmpty(&out, in.PriorCategory)
|
||||||
|
|||||||
@@ -261,6 +261,8 @@ func (p *Pipeline) LoadV1ProcessJobItems(ctx context.Context, companyID, jobID u
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
_ = processedID
|
||||||
|
_ = rawProductID
|
||||||
if processedID == nil {
|
if processedID == nil {
|
||||||
st := MapV1JobItemStatus(itemStatus, false)
|
st := MapV1JobItemStatus(itemStatus, false)
|
||||||
if st == "processed" || st == "processing" || st == "pending" {
|
if st == "processed" || st == "processing" || st == "pending" {
|
||||||
@@ -274,7 +276,6 @@ func (p *Pipeline) LoadV1ProcessJobItems(ctx context.Context, companyID, jobID u
|
|||||||
if itemError != nil && *itemError != "" {
|
if itemError != nil && *itemError != "" {
|
||||||
item["error"] = *itemError
|
item["error"] = *itemError
|
||||||
}
|
}
|
||||||
applyV1ProcessItemIDs(item, nil, rawProductID)
|
|
||||||
full = append(full, item)
|
full = append(full, item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -428,9 +429,9 @@ func (p *Pipeline) LoadV1ProcessJobItems(ctx context.Context, companyID, jobID u
|
|||||||
catIDOut = catStr
|
catIDOut = catStr
|
||||||
}
|
}
|
||||||
|
|
||||||
var titleOut any
|
var nameOut any
|
||||||
if titleStr != "" {
|
if titleStr != "" {
|
||||||
titleOut = titleStr
|
nameOut = titleStr
|
||||||
}
|
}
|
||||||
item := V1ProcessJobItem{
|
item := V1ProcessJobItem{
|
||||||
"ean": ean,
|
"ean": ean,
|
||||||
@@ -438,8 +439,7 @@ func (p *Pipeline) LoadV1ProcessJobItems(ctx context.Context, companyID, jobID u
|
|||||||
"category": catNameOut,
|
"category": catNameOut,
|
||||||
"category_id": catIDOut,
|
"category_id": catIDOut,
|
||||||
"category_name": catNameOut,
|
"category_name": catNameOut,
|
||||||
"title": titleOut,
|
"name": nameOut,
|
||||||
"name": titleOut,
|
|
||||||
"description": description,
|
"description": description,
|
||||||
"attributes": nil,
|
"attributes": nil,
|
||||||
"main_image": nil,
|
"main_image": nil,
|
||||||
@@ -450,7 +450,8 @@ func (p *Pipeline) LoadV1ProcessJobItems(ctx context.Context, companyID, jobID u
|
|||||||
item["meta_title"] = metaTitleOut
|
item["meta_title"] = metaTitleOut
|
||||||
item["meta_description"] = metaDescOut
|
item["meta_description"] = metaDescOut
|
||||||
}
|
}
|
||||||
applyV1ProcessItemIDs(item, processedID, rawProductID)
|
// Internal ids (id / processed_product_id / raw_product_id) are omitted from
|
||||||
|
// the public V1 process item shape — use catalog APIs when a UUID is needed.
|
||||||
if itemError != nil && *itemError != "" {
|
if itemError != nil && *itemError != "" {
|
||||||
item["error"] = *itemError
|
item["error"] = *itemError
|
||||||
}
|
}
|
||||||
@@ -483,20 +484,41 @@ func nullIfEmptyPtr(s *string) any {
|
|||||||
return *s
|
return *s
|
||||||
}
|
}
|
||||||
|
|
||||||
// companyOmitsSEOMeta is true for the A1 cohort (no meta_title / meta_description).
|
// companyOmitsSEOMeta is true when V1/process must omit meta_title / meta_description:
|
||||||
|
// A1 cohort (legacy id), Platform Demo (A1-cloned prompts, no legacy id), or any
|
||||||
|
// company whose categories store A1-style role-section enhance prompts.
|
||||||
func companyOmitsSEOMeta(ctx context.Context, p *Pipeline, companyID uuid.UUID) bool {
|
func companyOmitsSEOMeta(ctx context.Context, p *Pipeline, companyID uuid.UUID) bool {
|
||||||
if p == nil || p.Pool == nil {
|
if p == nil || p.Pool == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var legacy string
|
var legacy, name string
|
||||||
err := p.Pool.QueryRow(ctx, `
|
err := p.Pool.QueryRow(ctx, `
|
||||||
SELECT COALESCE(legacy_company_id, '')
|
SELECT COALESCE(legacy_company_id, ''), COALESCE(name, '')
|
||||||
FROM companies
|
FROM companies
|
||||||
WHERE id = $1`, companyID).Scan(&legacy)
|
WHERE id = $1`, companyID).Scan(&legacy, &name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return billing.IsA1CohortCompany(legacy, "")
|
if billing.IsA1CohortCompany(legacy, "") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(name), "Platform Demo") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var hasA1Prompts bool
|
||||||
|
err = p.Pool.QueryRow(ctx, `
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM categories
|
||||||
|
WHERE company_id = $1
|
||||||
|
AND prompt ILIKE '%--- Title ---%'
|
||||||
|
AND prompt ILIKE '%--- Description ---%'
|
||||||
|
AND prompt ILIKE '%--- Meta ---%'
|
||||||
|
LIMIT 1
|
||||||
|
)`, companyID).Scan(&hasA1Prompts)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasA1Prompts
|
||||||
}
|
}
|
||||||
|
|
||||||
func derefStringPtr(s *string) string {
|
func derefStringPtr(s *string) string {
|
||||||
@@ -724,10 +746,14 @@ func ProjectV1ProcessJobItems(storedType string, items []V1ProcessJobItem) []V1P
|
|||||||
projected["category_id"] = item["category_id"]
|
projected["category_id"] = item["category_id"]
|
||||||
projected["category_name"] = item["category_name"]
|
projected["category_name"] = item["category_name"]
|
||||||
case "title":
|
case "title":
|
||||||
projected["title"] = item["title"]
|
name := item["name"]
|
||||||
projected["name"] = item["name"]
|
if name == nil {
|
||||||
if projected["name"] == nil {
|
name = item["title"]
|
||||||
projected["name"] = item["title"]
|
}
|
||||||
|
projected["name"] = name
|
||||||
|
// Omit duplicate title when it matches name (name is primary).
|
||||||
|
if title := item["title"]; title != nil && title != name {
|
||||||
|
projected["title"] = title
|
||||||
}
|
}
|
||||||
if _, ok := item["meta_title"]; ok {
|
if _, ok := item["meta_title"]; ok {
|
||||||
projected["meta_title"] = item["meta_title"]
|
projected["meta_title"] = item["meta_title"]
|
||||||
@@ -747,7 +773,7 @@ func ProjectV1ProcessJobItems(storedType string, items []V1ProcessJobItem) []V1P
|
|||||||
}
|
}
|
||||||
|
|
||||||
func withItemMeta(dst, src V1ProcessJobItem) V1ProcessJobItem {
|
func withItemMeta(dst, src V1ProcessJobItem) V1ProcessJobItem {
|
||||||
for _, k := range []string{"status", "error", "id", "processed_product_id", "raw_product_id"} {
|
for _, k := range []string{"status", "error"} {
|
||||||
if v, ok := src[k]; ok {
|
if v, ok := src[k]; ok {
|
||||||
dst[k] = v
|
dst[k] = v
|
||||||
}
|
}
|
||||||
@@ -755,17 +781,13 @@ func withItemMeta(dst, src V1ProcessJobItem) V1ProcessJobItem {
|
|||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyV1ProcessItemIDs sets legacy id (= processed UUID) plus additive dual-mode aliases.
|
// applyV1ProcessItemIDs formerly stamped id / processed_product_id / raw_product_id onto
|
||||||
// id is preserved for existing integrators; processed_product_id mirrors it; raw_product_id is raw_products.id.
|
// V1 process items. Those fields are omitted from the public contract; kept as a no-op
|
||||||
|
// so older call sites/tests compile until removed.
|
||||||
func applyV1ProcessItemIDs(item V1ProcessJobItem, processedID, rawProductID *uuid.UUID) {
|
func applyV1ProcessItemIDs(item V1ProcessJobItem, processedID, rawProductID *uuid.UUID) {
|
||||||
if processedID != nil {
|
_ = item
|
||||||
s := processedID.String()
|
_ = processedID
|
||||||
item["id"] = s
|
_ = rawProductID
|
||||||
item["processed_product_id"] = s
|
|
||||||
}
|
|
||||||
if rawProductID != nil {
|
|
||||||
item["raw_product_id"] = rawProductID.String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func withAlwaysIncluded(item V1ProcessJobItem, main string, more []string, eprel any) V1ProcessJobItem {
|
func withAlwaysIncluded(item V1ProcessJobItem, main string, more []string, eprel any) V1ProcessJobItem {
|
||||||
|
|||||||
@@ -72,11 +72,8 @@ func TestMapV1JobItemStatus(t *testing.T) {
|
|||||||
|
|
||||||
func TestProjectV1ProcessJobItemsPartial(t *testing.T) {
|
func TestProjectV1ProcessJobItemsPartial(t *testing.T) {
|
||||||
items := []V1ProcessJobItem{{
|
items := []V1ProcessJobItem{{
|
||||||
"ean": "123", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
"ean": "123", "status": "processed",
|
||||||
"processed_product_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
"name": "T", "meta_title": "MT",
|
||||||
"raw_product_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
|
|
||||||
"status": "processed",
|
|
||||||
"title": "T", "name": "T", "meta_title": "MT",
|
|
||||||
"description": "D", "attributes": map[string]any{"brand": "X"},
|
"description": "D", "attributes": map[string]any{"brand": "X"},
|
||||||
"main_image": "https://example.com/a.jpg", "more_images": []string{"https://example.com/b.jpg"},
|
"main_image": "https://example.com/a.jpg", "more_images": []string{"https://example.com/b.jpg"},
|
||||||
"eprel": nil, "category": "cat", "category_name": "Cat",
|
"eprel": nil, "category": "cat", "category_name": "Cat",
|
||||||
@@ -88,11 +85,11 @@ func TestProjectV1ProcessJobItemsPartial(t *testing.T) {
|
|||||||
raw, _ := json.Marshal(out[0])
|
raw, _ := json.Marshal(out[0])
|
||||||
var got map[string]any
|
var got map[string]any
|
||||||
_ = json.Unmarshal(raw, &got)
|
_ = json.Unmarshal(raw, &got)
|
||||||
if got["title"] != "T" || got["ean"] != "123" {
|
if got["name"] != "T" || got["ean"] != "123" {
|
||||||
t.Fatalf("got=%v", got)
|
t.Fatalf("got=%v", got)
|
||||||
}
|
}
|
||||||
if got["name"] != "T" {
|
if _, ok := got["title"]; ok {
|
||||||
t.Fatalf("name dual-mode alias missing or mismatched: %v", got)
|
t.Fatalf("duplicate title should be omitted when name exists: %v", got)
|
||||||
}
|
}
|
||||||
if _, ok := got["attributes"]; ok {
|
if _, ok := got["attributes"]; ok {
|
||||||
t.Fatalf("attributes should be projected out: %v", got)
|
t.Fatalf("attributes should be projected out: %v", got)
|
||||||
@@ -100,31 +97,29 @@ func TestProjectV1ProcessJobItemsPartial(t *testing.T) {
|
|||||||
if got["main_image"] != "https://example.com/a.jpg" {
|
if got["main_image"] != "https://example.com/a.jpg" {
|
||||||
t.Fatalf("images always included: %v", got)
|
t.Fatalf("images always included: %v", got)
|
||||||
}
|
}
|
||||||
if got["status"] != "processed" || got["id"] != "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" {
|
if got["status"] != "processed" {
|
||||||
t.Fatalf("meta should be preserved: %v", got)
|
t.Fatalf("status should be preserved: %v", got)
|
||||||
}
|
}
|
||||||
if got["processed_product_id"] != "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" {
|
for _, junk := range []string{"id", "processed_product_id", "raw_product_id"} {
|
||||||
t.Fatalf("processed_product_id should be preserved: %v", got)
|
if _, ok := got[junk]; ok {
|
||||||
|
t.Fatalf("%s must be omitted from V1 process items: %v", junk, got)
|
||||||
}
|
}
|
||||||
if got["raw_product_id"] != "cccccccc-cccc-cccc-cccc-cccccccccccc" {
|
|
||||||
t.Fatalf("raw_product_id should be preserved: %v", got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProjectV1ProcessJobItemsTitleDerivesName(t *testing.T) {
|
func TestProjectV1ProcessJobItemsTitleDerivesName(t *testing.T) {
|
||||||
items := []V1ProcessJobItem{{
|
items := []V1ProcessJobItem{{
|
||||||
"ean": "123", "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
"ean": "123", "status": "processed", "title": "OnlyTitle", "meta_title": "MT",
|
||||||
"status": "processed", "title": "OnlyTitle", "meta_title": "MT",
|
|
||||||
}}
|
}}
|
||||||
out := ProjectV1ProcessJobItems("title", items)
|
out := ProjectV1ProcessJobItems("title", items)
|
||||||
if len(out) != 1 {
|
if len(out) != 1 {
|
||||||
t.Fatalf("len=%d", len(out))
|
t.Fatalf("len=%d", len(out))
|
||||||
}
|
}
|
||||||
if out[0]["title"] != "OnlyTitle" {
|
|
||||||
t.Fatalf("title=%v", out[0]["title"])
|
|
||||||
}
|
|
||||||
if out[0]["name"] != "OnlyTitle" {
|
if out[0]["name"] != "OnlyTitle" {
|
||||||
t.Fatalf("name should mirror title when absent: %v", out[0]["name"])
|
t.Fatalf("name should derive from title when absent: %v", out[0]["name"])
|
||||||
|
}
|
||||||
|
if _, ok := out[0]["title"]; ok {
|
||||||
|
t.Fatalf("title omitted when identical to name: %v", out[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,22 +175,18 @@ func TestApplyV1ProcessItemIDsDualMode(t *testing.T) {
|
|||||||
raw := mustParseTestUUID(t, "cccccccc-cccc-cccc-cccc-cccccccccccc")
|
raw := mustParseTestUUID(t, "cccccccc-cccc-cccc-cccc-cccccccccccc")
|
||||||
item := V1ProcessJobItem{"ean": "1", "status": "processed"}
|
item := V1ProcessJobItem{"ean": "1", "status": "processed"}
|
||||||
applyV1ProcessItemIDs(item, &processed, &raw)
|
applyV1ProcessItemIDs(item, &processed, &raw)
|
||||||
if item["id"] != processed.String() {
|
for _, junk := range []string{"id", "processed_product_id", "raw_product_id"} {
|
||||||
t.Fatalf("id=%v", item["id"])
|
if _, ok := item[junk]; ok {
|
||||||
|
t.Fatalf("%s must stay omitted from V1 process items: %v", junk, item)
|
||||||
}
|
}
|
||||||
if item["processed_product_id"] != processed.String() {
|
|
||||||
t.Fatalf("processed_product_id=%v", item["processed_product_id"])
|
|
||||||
}
|
|
||||||
if item["raw_product_id"] != raw.String() {
|
|
||||||
t.Fatalf("raw_product_id=%v", item["raw_product_id"])
|
|
||||||
}
|
}
|
||||||
missing := V1ProcessJobItem{"ean": "2", "status": "not_found"}
|
missing := V1ProcessJobItem{"ean": "2", "status": "not_found"}
|
||||||
applyV1ProcessItemIDs(missing, nil, &raw)
|
applyV1ProcessItemIDs(missing, nil, &raw)
|
||||||
if _, ok := missing["id"]; ok {
|
if _, ok := missing["id"]; ok {
|
||||||
t.Fatalf("id must stay absent without processed row: %v", missing)
|
t.Fatalf("id must stay absent: %v", missing)
|
||||||
}
|
}
|
||||||
if missing["raw_product_id"] != raw.String() {
|
if _, ok := missing["raw_product_id"]; ok {
|
||||||
t.Fatalf("raw_product_id on not_found: %v", missing)
|
t.Fatalf("raw_product_id must stay omitted: %v", missing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,11 +63,17 @@ func ScoreV1ProcessCompletedItem(item V1ProcessJobItem, opts ScoreV1ProcessItemO
|
|||||||
return sc
|
return sc
|
||||||
}
|
}
|
||||||
|
|
||||||
title := stringFromItem(item, "title")
|
title := stringFromItem(item, "name")
|
||||||
|
if title == "" {
|
||||||
|
title = stringFromItem(item, "title")
|
||||||
|
}
|
||||||
name := stringFromItem(item, "name")
|
name := stringFromItem(item, "name")
|
||||||
|
if name == "" {
|
||||||
|
name = title
|
||||||
|
}
|
||||||
sc.HasTitle = title != ""
|
sc.HasTitle = title != ""
|
||||||
sc.HasName = name != ""
|
sc.HasName = name != ""
|
||||||
if sc.HasTitle && !sc.HasName {
|
if !sc.HasName && sc.HasTitle {
|
||||||
sc.FailFlags = append(sc.FailFlags, "missing_name")
|
sc.FailFlags = append(sc.FailFlags, "missing_name")
|
||||||
}
|
}
|
||||||
if sc.HasTitle && sc.HasName && title != name {
|
if sc.HasTitle && sc.HasName && title != name {
|
||||||
@@ -140,22 +146,8 @@ func ScoreV1ProcessCompletedItem(item V1ProcessJobItem, opts ScoreV1ProcessItemO
|
|||||||
sc.FailFlags = append(sc.FailFlags, "eprel_invalid_shape")
|
sc.FailFlags = append(sc.FailFlags, "eprel_invalid_shape")
|
||||||
}
|
}
|
||||||
|
|
||||||
id := stringFromItem(item, "id")
|
// Internal UUIDs are omitted from the public V1 process item shape.
|
||||||
ppID := stringFromItem(item, "processed_product_id")
|
sc.HasIDs = true
|
||||||
rawID := stringFromItem(item, "raw_product_id")
|
|
||||||
sc.HasIDs = id != "" && ppID != "" && rawID != ""
|
|
||||||
if id == "" {
|
|
||||||
sc.FailFlags = append(sc.FailFlags, "missing_id")
|
|
||||||
}
|
|
||||||
if ppID == "" {
|
|
||||||
sc.FailFlags = append(sc.FailFlags, "missing_processed_product_id")
|
|
||||||
}
|
|
||||||
if rawID == "" {
|
|
||||||
sc.FailFlags = append(sc.FailFlags, "missing_raw_product_id")
|
|
||||||
}
|
|
||||||
if id != "" && ppID != "" && id != ppID {
|
|
||||||
sc.FailFlags = append(sc.FailFlags, "id_processed_product_id_mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
sc.ImagesOK = imageFieldsOK(item)
|
sc.ImagesOK = imageFieldsOK(item)
|
||||||
if !sc.ImagesOK {
|
if !sc.ImagesOK {
|
||||||
@@ -234,14 +226,20 @@ func EnforceV1ProcessCompletedItemOpts(item V1ProcessJobItem, opts EnforceV1Opts
|
|||||||
item["attributes"] = nil
|
item["attributes"] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
title := ensureReadableTitleSpacing(stringFromItem(item, "title"))
|
title := ensureReadableTitleSpacing(stringFromItem(item, "name"))
|
||||||
if title != "" {
|
if title == "" {
|
||||||
item["title"] = title
|
title = ensureReadableTitleSpacing(stringFromItem(item, "title"))
|
||||||
item["name"] = title
|
|
||||||
} else {
|
|
||||||
item["title"] = nil
|
|
||||||
item["name"] = nil
|
|
||||||
}
|
}
|
||||||
|
if title != "" {
|
||||||
|
item["name"] = title
|
||||||
|
delete(item, "title")
|
||||||
|
} else {
|
||||||
|
item["name"] = nil
|
||||||
|
delete(item, "title")
|
||||||
|
}
|
||||||
|
delete(item, "id")
|
||||||
|
delete(item, "processed_product_id")
|
||||||
|
delete(item, "raw_product_id")
|
||||||
|
|
||||||
catID, catName := projectV1CategoryFields(item)
|
catID, catName := projectV1CategoryFields(item)
|
||||||
catLabel := catName
|
catLabel := catName
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ func TestScoreV1ProcessCompletedItem_flagsGaps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
joined := strings.Join(sc.FailFlags, ",")
|
joined := strings.Join(sc.FailFlags, ",")
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
"missing_name",
|
|
||||||
"description_empty_with_title",
|
"description_empty_with_title",
|
||||||
"meta_title_missing",
|
"meta_title_missing",
|
||||||
"meta_description_missing",
|
"meta_description_missing",
|
||||||
@@ -103,7 +102,7 @@ func TestEnforceV1ProcessCompletedItem_fillsDescriptionMetaSanitizes(t *testing.
|
|||||||
if desc == "" {
|
if desc == "" {
|
||||||
t.Fatal("expected nonempty description")
|
t.Fatal("expected nonempty description")
|
||||||
}
|
}
|
||||||
if descriptionEchoesTitle(desc, fmt.Sprint(out["title"])) {
|
if descriptionEchoesTitle(desc, fmt.Sprint(out["name"])) {
|
||||||
t.Fatalf("EnforceV1 must replace weak/echo desc, got title-echo: %q", desc)
|
t.Fatalf("EnforceV1 must replace weak/echo desc, got title-echo: %q", desc)
|
||||||
}
|
}
|
||||||
if containsWeakFillerPhrase(desc) {
|
if containsWeakFillerPhrase(desc) {
|
||||||
@@ -131,8 +130,16 @@ func TestEnforceV1ProcessCompletedItem_fillsDescriptionMetaSanitizes(t *testing.
|
|||||||
if _, bad := attrs["name"]; bad {
|
if _, bad := attrs["name"]; bad {
|
||||||
t.Fatalf("reserved name kept: %v", attrs)
|
t.Fatalf("reserved name kept: %v", attrs)
|
||||||
}
|
}
|
||||||
if out["name"] != out["title"] {
|
if out["name"] == nil || strings.TrimSpace(fmt.Sprint(out["name"])) == "" {
|
||||||
t.Fatalf("name should mirror title: name=%v title=%v", out["name"], out["title"])
|
t.Fatalf("name missing: %v", out["name"])
|
||||||
|
}
|
||||||
|
if _, ok := out["title"]; ok {
|
||||||
|
t.Fatalf("duplicate title should be omitted when name exists: %v", out["title"])
|
||||||
|
}
|
||||||
|
for _, junk := range []string{"id", "processed_product_id", "raw_product_id"} {
|
||||||
|
if _, ok := out[junk]; ok {
|
||||||
|
t.Fatalf("%s must be omitted: %v", junk, out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sc := ScoreV1ProcessCompletedItem(out, ScoreV1ProcessItemOptions{MappedCategory: "28"})
|
sc := ScoreV1ProcessCompletedItem(out, ScoreV1ProcessItemOptions{MappedCategory: "28"})
|
||||||
if !sc.OK {
|
if !sc.OK {
|
||||||
|
|||||||
@@ -285,36 +285,15 @@ export type FixCatalogResult = SyncA1Result;
|
|||||||
/** @deprecated Use SyncA1Response */
|
/** @deprecated Use SyncA1Response */
|
||||||
export type FixCatalogResponse = SyncA1Response;
|
export type FixCatalogResponse = SyncA1Response;
|
||||||
|
|
||||||
/** Sync A1: optional wp_product_categories.sql upload + dump category backfill + Fix hygiene. */
|
/** Sync A1: dump category backfill + Fix hygiene; category prompts load from repo seed. */
|
||||||
export async function syncAdminCompanyA1(
|
export async function syncAdminCompanyA1(
|
||||||
companyId: string,
|
companyId: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
backfillCategories?: boolean;
|
backfillCategories?: boolean;
|
||||||
reprocessSampleLimit?: number;
|
reprocessSampleLimit?: number;
|
||||||
skipDumpBackfill?: boolean;
|
skipDumpBackfill?: boolean;
|
||||||
wpProductCategoriesFile?: File | null;
|
|
||||||
}
|
}
|
||||||
): Promise<SyncA1Response> {
|
): Promise<SyncA1Response> {
|
||||||
const file = opts?.wpProductCategoriesFile ?? null;
|
|
||||||
if (file) {
|
|
||||||
const body = new FormData();
|
|
||||||
body.append("confirm", "true");
|
|
||||||
if (opts?.backfillCategories === false) {
|
|
||||||
body.append("backfill_categories", "false");
|
|
||||||
}
|
|
||||||
if (typeof opts?.reprocessSampleLimit === "number") {
|
|
||||||
body.append("reprocess_sample_limit", String(opts.reprocessSampleLimit));
|
|
||||||
}
|
|
||||||
if (opts?.skipDumpBackfill) {
|
|
||||||
body.append("skip_dump_backfill", "true");
|
|
||||||
}
|
|
||||||
body.append("wp_product_categories", file, file.name || "wp_product_categories.sql");
|
|
||||||
return api<SyncA1Response>(ADMIN_SYNC_A1_PATH(companyId), {
|
|
||||||
method: "POST",
|
|
||||||
body
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const body: {
|
const body: {
|
||||||
confirm: true;
|
confirm: true;
|
||||||
backfill_categories?: boolean;
|
backfill_categories?: boolean;
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const de: MessageDict = {
|
|||||||
"admin.users.syncA1": "A1 synchronisieren",
|
"admin.users.syncA1": "A1 synchronisieren",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "A1-Katalog synchronisieren",
|
"admin.users.syncA1Title": "A1-Katalog synchronisieren",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Abbrechen",
|
"admin.users.syncA1Cancel": "Abbrechen",
|
||||||
"admin.users.syncA1Confirm": "A1 synchronisieren",
|
"admin.users.syncA1Confirm": "A1 synchronisieren",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Datei entfernen",
|
|
||||||
"admin.users.noUsers": "Keine Benutzer entsprechen diesem Filter.",
|
"admin.users.noUsers": "Keine Benutzer entsprechen diesem Filter.",
|
||||||
"admin.users.noCompanies": "Keine Unternehmen entsprechen diesem Filter.",
|
"admin.users.noCompanies": "Keine Unternehmen entsprechen diesem Filter.",
|
||||||
"admin.users.assignRoleTitle": "Mitarbeiterrolle zuweisen",
|
"admin.users.assignRoleTitle": "Mitarbeiterrolle zuweisen",
|
||||||
|
|||||||
@@ -856,14 +856,11 @@ export const en: MessageDict = {
|
|||||||
"admin.users.syncA1": "Sync A1",
|
"admin.users.syncA1": "Sync A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Sync A1 catalog",
|
"admin.users.syncA1Title": "Sync A1 catalog",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Cancel",
|
"admin.users.syncA1Cancel": "Cancel",
|
||||||
"admin.users.syncA1Confirm": "Sync A1",
|
"admin.users.syncA1Confirm": "Sync A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Clear file",
|
|
||||||
"admin.users.noUsers": "No users match this filter.",
|
"admin.users.noUsers": "No users match this filter.",
|
||||||
"admin.users.noCompanies": "No companies match this filter.",
|
"admin.users.noCompanies": "No companies match this filter.",
|
||||||
"admin.users.assignRoleTitle": "Assign staff role",
|
"admin.users.assignRoleTitle": "Assign staff role",
|
||||||
@@ -2109,8 +2106,8 @@ export const en: MessageDict = {
|
|||||||
"settings.emailAlertsHelp": "Email alert preferences are coming soon. Failure alerts will default on (like in-app); completion emails stay off unless you opt in.",
|
"settings.emailAlertsHelp": "Email alert preferences are coming soon. Failure alerts will default on (like in-app); completion emails stay off unless you opt in.",
|
||||||
"settings.emailAlertsBody": "For now, configure transactional mail under Company → Email integration. In-app alerts above still apply.",
|
"settings.emailAlertsBody": "For now, configure transactional mail under Company → Email integration. In-app alerts above still apply.",
|
||||||
"settings.apiKeysLegacyNotice": "Legacy API keys were not migrated. Create a new key to restore API access — the secret is shown only once.",
|
"settings.apiKeysLegacyNotice": "Legacy API keys were not migrated. Create a new key to restore API access — the secret is shown only once.",
|
||||||
"settings.apiKeysAdminOnly": "Only company admins can create or revoke API keys. You can view existing key prefixes.",
|
"settings.apiKeysAdminOnly": "Only company admins can create, rename, or revoke API keys. You can view existing key prefixes.",
|
||||||
"settings.apiKeysCopyUnavailableNotice": "Copy unavailable for existing keys — only the prefix is stored. Create a new key to copy the full secret once, then revoke the old key if needed.",
|
"settings.apiKeysCopyUnavailableNotice": "After you create a key, copy the full secret immediately (or from the row menu while this page stays open). Existing keys only store a prefix — create a new key if you lost the secret.",
|
||||||
"settings.apiKeysForbidden": "You don't have permission to view API keys. Ask a company admin for help.",
|
"settings.apiKeysForbidden": "You don't have permission to view API keys. Ask a company admin for help.",
|
||||||
"settings.noApiKeys": "No API keys yet",
|
"settings.noApiKeys": "No API keys yet",
|
||||||
"settings.noApiKeysHelpAdmin": "Legacy API keys were not migrated. Create a new key to authenticate Descrybe API requests (X-API-Key). The full secret is shown only once.",
|
"settings.noApiKeysHelpAdmin": "Legacy API keys were not migrated. Create a new key to authenticate Descrybe API requests (X-API-Key). The full secret is shown only once.",
|
||||||
@@ -2118,13 +2115,20 @@ export const en: MessageDict = {
|
|||||||
"settings.unnamedKey": "Unnamed Key",
|
"settings.unnamedKey": "Unnamed Key",
|
||||||
"settings.neverUsed": "Never used",
|
"settings.neverUsed": "Never used",
|
||||||
"settings.apiKeyActions": "API key actions",
|
"settings.apiKeyActions": "API key actions",
|
||||||
|
"settings.renameApiKey": "Rename",
|
||||||
|
"settings.renameApiKeyTitle": "Rename API key",
|
||||||
|
"settings.renameApiKeyHelp": "Update the display name for this key. The secret itself does not change.",
|
||||||
|
"settings.saveRename": "Save name",
|
||||||
|
"settings.copyApiKey": "Copy secret",
|
||||||
"settings.copyUnavailable": "Copy unavailable",
|
"settings.copyUnavailable": "Copy unavailable",
|
||||||
"settings.copyUnavailableTitle": "Full secret is shown only once when the key is created",
|
"settings.copyUnavailableTitle": "Full secret is shown only once when the key is created (kept copyable on this page until you refresh)",
|
||||||
"settings.revoke": "Revoke",
|
"settings.revoke": "Revoke",
|
||||||
"settings.apiKeyNameRequired": "Enter a name for this API key.",
|
"settings.apiKeyNameRequired": "Enter a name for this API key.",
|
||||||
"settings.apiKeyCreatedOnce": "API key created. Copy it now - it won't be shown again.",
|
"settings.apiKeyCreatedOnce": "API key created. Copy it now - it won't be shown again.",
|
||||||
"settings.apiKeyCreated": "API key created.",
|
"settings.apiKeyCreated": "API key created.",
|
||||||
"settings.apiKeyCreateFailed": "Could not create API key",
|
"settings.apiKeyCreateFailed": "Could not create API key",
|
||||||
|
"settings.apiKeyRenamed": "API key renamed.",
|
||||||
|
"settings.apiKeyRenameFailed": "Could not rename API key",
|
||||||
"settings.apiKeyRevokeConfirm": "Revoke this API key? Requests using it will stop working.",
|
"settings.apiKeyRevokeConfirm": "Revoke this API key? Requests using it will stop working.",
|
||||||
"settings.apiKeyRevoked": "API key revoked.",
|
"settings.apiKeyRevoked": "API key revoked.",
|
||||||
"settings.apiKeyRevokeFailed": "Could not revoke API key",
|
"settings.apiKeyRevokeFailed": "Could not revoke API key",
|
||||||
@@ -2282,6 +2286,7 @@ export const en: MessageDict = {
|
|||||||
"flash.settings.apiKeyCreatedFailed": "Could not create API key",
|
"flash.settings.apiKeyCreatedFailed": "Could not create API key",
|
||||||
"flash.settings.apiKeyRevoked": "API key revoked.",
|
"flash.settings.apiKeyRevoked": "API key revoked.",
|
||||||
"flash.settings.apiKeyRevokeFailed": "Could not revoke API key",
|
"flash.settings.apiKeyRevokeFailed": "Could not revoke API key",
|
||||||
|
"flash.settings.apiKeyRenameFailed": "Could not rename API key",
|
||||||
"flash.settings.copyFailed": "Could not copy. Select the text and copy manually.",
|
"flash.settings.copyFailed": "Could not copy. Select the text and copy manually.",
|
||||||
"flash.products.accepted": "Accepted {count} product(s) — moved to Processed.",
|
"flash.products.accepted": "Accepted {count} product(s) — moved to Processed.",
|
||||||
"flash.products.updated": "Product updated.",
|
"flash.products.updated": "Product updated.",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const es: MessageDict = {
|
|||||||
"admin.users.syncA1": "Sincronizar A1",
|
"admin.users.syncA1": "Sincronizar A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Sincronizar catálogo A1",
|
"admin.users.syncA1Title": "Sincronizar catálogo A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Cancelar",
|
"admin.users.syncA1Cancel": "Cancelar",
|
||||||
"admin.users.syncA1Confirm": "Sincronizar A1",
|
"admin.users.syncA1Confirm": "Sincronizar A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Quitar archivo",
|
|
||||||
"admin.users.noUsers": "Ningún usuario coincide con este filtro.",
|
"admin.users.noUsers": "Ningún usuario coincide con este filtro.",
|
||||||
"admin.users.noCompanies": "Ninguna empresa coincide con este filtro.",
|
"admin.users.noCompanies": "Ninguna empresa coincide con este filtro.",
|
||||||
"admin.users.assignRoleTitle": "Asignar rol de personal",
|
"admin.users.assignRoleTitle": "Asignar rol de personal",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const fr: MessageDict = {
|
|||||||
"admin.users.syncA1": "Synchroniser A1",
|
"admin.users.syncA1": "Synchroniser A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Synchroniser le catalogue A1",
|
"admin.users.syncA1Title": "Synchroniser le catalogue A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Annuler",
|
"admin.users.syncA1Cancel": "Annuler",
|
||||||
"admin.users.syncA1Confirm": "Synchroniser A1",
|
"admin.users.syncA1Confirm": "Synchroniser A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Effacer le fichier",
|
|
||||||
"admin.users.noUsers": "Aucun utilisateur ne correspond àce filtre.",
|
"admin.users.noUsers": "Aucun utilisateur ne correspond àce filtre.",
|
||||||
"admin.users.noCompanies": "Aucune entreprise ne correspond àce filtre.",
|
"admin.users.noCompanies": "Aucune entreprise ne correspond àce filtre.",
|
||||||
"admin.users.assignRoleTitle": "Assigner un rôle du personnel",
|
"admin.users.assignRoleTitle": "Assigner un rôle du personnel",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const it: MessageDict = {
|
|||||||
"admin.users.syncA1": "Sincronizza A1",
|
"admin.users.syncA1": "Sincronizza A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Sincronizza catalogo A1",
|
"admin.users.syncA1Title": "Sincronizza catalogo A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Annulla",
|
"admin.users.syncA1Cancel": "Annulla",
|
||||||
"admin.users.syncA1Confirm": "Sincronizza A1",
|
"admin.users.syncA1Confirm": "Sincronizza A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Rimuovi file",
|
|
||||||
"admin.users.noUsers": "Nessun utente corrisponde a questo filtro.",
|
"admin.users.noUsers": "Nessun utente corrisponde a questo filtro.",
|
||||||
"admin.users.noCompanies": "Nessuna azienda corrisponde a questo filtro.",
|
"admin.users.noCompanies": "Nessuna azienda corrisponde a questo filtro.",
|
||||||
"admin.users.assignRoleTitle": "Assegna ruolo staff",
|
"admin.users.assignRoleTitle": "Assegna ruolo staff",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const ja: MessageDict = {
|
|||||||
"admin.users.syncA1": "A1ã‚’åŒæœŸ",
|
"admin.users.syncA1": "A1ã‚’åŒæœŸ",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "A1ã‚«ã‚¿ãƒã‚°ã‚’åŒæœŸ",
|
"admin.users.syncA1Title": "A1ã‚«ã‚¿ãƒã‚°ã‚’åŒæœŸ",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "ã‚ャンセル",
|
"admin.users.syncA1Cancel": "ã‚ャンセル",
|
||||||
"admin.users.syncA1Confirm": "A1ã‚’åŒæœŸ",
|
"admin.users.syncA1Confirm": "A1ã‚’åŒæœŸ",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "ファイルをクリア",
|
|
||||||
"admin.users.noUsers": "ã“ã®フィルタã«一致ã™るユーザーã¯ã„ã¾ã›ん。",
|
"admin.users.noUsers": "ã“ã®フィルタã«一致ã™るユーザーã¯ã„ã¾ã›ん。",
|
||||||
"admin.users.noCompanies": "ã“ã®フィルタã«一致ã™る会社ã¯ã‚りã¾ã›ん。",
|
"admin.users.noCompanies": "ã“ã®フィルタã«一致ã™る会社ã¯ã‚りã¾ã›ん。",
|
||||||
"admin.users.assignRoleTitle": "スタッフãƒÂールを割り当ã¦",
|
"admin.users.assignRoleTitle": "スタッフãƒÂールを割り当ã¦",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const nl: MessageDict = {
|
|||||||
"admin.users.syncA1": "A1 synchroniseren",
|
"admin.users.syncA1": "A1 synchroniseren",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "A1-catalogus synchroniseren",
|
"admin.users.syncA1Title": "A1-catalogus synchroniseren",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Annuleren",
|
"admin.users.syncA1Cancel": "Annuleren",
|
||||||
"admin.users.syncA1Confirm": "A1 synchroniseren",
|
"admin.users.syncA1Confirm": "A1 synchroniseren",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Bestand wissen",
|
|
||||||
"admin.users.noUsers": "Geen gebruikers komen overeen met dit filter.",
|
"admin.users.noUsers": "Geen gebruikers komen overeen met dit filter.",
|
||||||
"admin.users.noCompanies": "Geen bedrijven komen overeen met dit filter.",
|
"admin.users.noCompanies": "Geen bedrijven komen overeen met dit filter.",
|
||||||
"admin.users.assignRoleTitle": "Medewerkerrol toewijzen",
|
"admin.users.assignRoleTitle": "Medewerkerrol toewijzen",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const pl: MessageDict = {
|
|||||||
"admin.users.syncA1": "Synchronizuj A1",
|
"admin.users.syncA1": "Synchronizuj A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Synchronizuj katalog A1",
|
"admin.users.syncA1Title": "Synchronizuj katalog A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Anuluj",
|
"admin.users.syncA1Cancel": "Anuluj",
|
||||||
"admin.users.syncA1Confirm": "Synchronizuj A1",
|
"admin.users.syncA1Confirm": "Synchronizuj A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Wyczyść plik",
|
|
||||||
"admin.users.noUsers": "Żaden użytkownik nie pasuje do tego filtra.",
|
"admin.users.noUsers": "Żaden użytkownik nie pasuje do tego filtra.",
|
||||||
"admin.users.noCompanies": "Żadna firma nie pasuje do tego filtra.",
|
"admin.users.noCompanies": "Żadna firma nie pasuje do tego filtra.",
|
||||||
"admin.users.assignRoleTitle": "Przypisz rolÄ™ personelu",
|
"admin.users.assignRoleTitle": "Przypisz rolÄ™ personelu",
|
||||||
|
|||||||
@@ -829,14 +829,11 @@ export const pt: MessageDict = {
|
|||||||
"admin.users.syncA1": "Sincronizar A1",
|
"admin.users.syncA1": "Sincronizar A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Sincronizar catálogo A1",
|
"admin.users.syncA1Title": "Sincronizar catálogo A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "Cancelar",
|
"admin.users.syncA1Cancel": "Cancelar",
|
||||||
"admin.users.syncA1Confirm": "Sincronizar A1",
|
"admin.users.syncA1Confirm": "Sincronizar A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Limpar ficheiro",
|
|
||||||
"admin.users.noUsers": "Nenhum utilizador corresponde a este filtro.",
|
"admin.users.noUsers": "Nenhum utilizador corresponde a este filtro.",
|
||||||
"admin.users.noCompanies": "Nenhuma empresa corresponde a este filtro.",
|
"admin.users.noCompanies": "Nenhuma empresa corresponde a este filtro.",
|
||||||
"admin.users.assignRoleTitle": "Atribuir função de equipa",
|
"admin.users.assignRoleTitle": "Atribuir função de equipa",
|
||||||
|
|||||||
@@ -18,14 +18,11 @@ export const sl: MessageDict = {
|
|||||||
"admin.users.syncA1": "Sinhroniziraj A1",
|
"admin.users.syncA1": "Sinhroniziraj A1",
|
||||||
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
"admin.users.syncA1Aria": "Sync A1 categories from dump and repair catalog hygiene for {name}",
|
||||||
"admin.users.syncA1Title": "Sinhroniziraj katalog A1",
|
"admin.users.syncA1Title": "Sinhroniziraj katalog A1",
|
||||||
"admin.users.syncA1Desc": "Uploads wp_product_categories.sql to force-apply Title/Description/Meta/Attributes category prompts, optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
"admin.users.syncA1Desc": "Applies Title/Description/Meta/Attributes category prompts from the repo seed (scripts/seed/wp_product_categories.sql), optionally backfills mapped categories from a MySQL product dump on the API host, then runs catalog hygiene. Does not wipe or reimport the full catalog.",
|
||||||
"admin.users.syncA1Company": "Company: {name}",
|
"admin.users.syncA1Company": "Company: {name}",
|
||||||
"admin.users.syncA1Warning": "Upload wp_product_categories.sql below (primary). Auto-detect on the API host is a fallback when no file is chosen. Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
"admin.users.syncA1Warning": "Category prompts load automatically from scripts/seed/wp_product_categories.sql on the API host (SEED_A1_WP_CATEGORIES overrides). Optional: place descrybe_new.sql on the API server for product-dump category coverage. No mass reprocess.",
|
||||||
"admin.users.syncA1Cancel": "PrekliÄi",
|
"admin.users.syncA1Cancel": "PrekliÄi",
|
||||||
"admin.users.syncA1Confirm": "Sinhroniziraj A1",
|
"admin.users.syncA1Confirm": "Sinhroniziraj A1",
|
||||||
"admin.users.syncA1WpUpload": "wp_product_categories.sql",
|
|
||||||
"admin.users.syncA1WpUploadHint": "Choose the WordPress category prompts dump. Max 16 MiB. When uploaded, Sync A1 force-applies sectioned prompts for title and description enhance.",
|
|
||||||
"admin.users.syncA1WpUploadClear": "Počisti datoteko",
|
|
||||||
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
|
"flash.admin.catalogCloned": "Copied {source} into {dest}: {products} products, {categories} taxonomy categories, {mapped_with} with mapped category.",
|
||||||
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processedâ†mapped {categories}, mappedâ†processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
|
"flash.admin.syncA1Success": "Synced A1 for {name}: dump_mapped {dump_mapped} ({dump_status}), prompts {prompts}, hashes {hashes}, processedâ†mapped {categories}, mappedâ†processed {mapped_backfilled}. Coverage: {mapped_with}/{mapped_total} ({taxonomy} taxonomy).",
|
||||||
"flash.admin.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
|
"flash.admin.syncA1Error": "Sinhronizacija A1 za {name} ni uspela.",
|
||||||
|
|||||||
@@ -100,7 +100,6 @@
|
|||||||
|
|
||||||
let syncOpen = $state(false);
|
let syncOpen = $state(false);
|
||||||
let syncCompany = $state<AdminOrgCompany | null>(null);
|
let syncCompany = $state<AdminOrgCompany | null>(null);
|
||||||
let syncWpCategoriesFile = $state<File | null>(null);
|
|
||||||
|
|
||||||
const cloneDestLabel = $derived.by(() => {
|
const cloneDestLabel = $derived.by(() => {
|
||||||
const selected = cloneDestOptions.find((c) => c.id === cloneDestId);
|
const selected = cloneDestOptions.find((c) => c.id === cloneDestId);
|
||||||
@@ -409,7 +408,6 @@
|
|||||||
|
|
||||||
function openSyncDialog(company: AdminOrgCompany) {
|
function openSyncDialog(company: AdminOrgCompany) {
|
||||||
syncCompany = company;
|
syncCompany = company;
|
||||||
syncWpCategoriesFile = null;
|
|
||||||
syncOpen = true;
|
syncOpen = true;
|
||||||
error = "";
|
error = "";
|
||||||
success = "";
|
success = "";
|
||||||
@@ -471,8 +469,7 @@
|
|||||||
const targetName = syncCompany.name;
|
const targetName = syncCompany.name;
|
||||||
try {
|
try {
|
||||||
const res = await syncAdminCompanyA1(syncCompany.id, {
|
const res = await syncAdminCompanyA1(syncCompany.id, {
|
||||||
reprocessSampleLimit: 25,
|
reprocessSampleLimit: 25
|
||||||
wpProductCategoriesFile: syncWpCategoriesFile
|
|
||||||
});
|
});
|
||||||
const r = res.result;
|
const r = res.result;
|
||||||
const mappedWith = Number(r.mapped_with_category ?? 0);
|
const mappedWith = Number(r.mapped_with_category ?? 0);
|
||||||
@@ -493,7 +490,6 @@
|
|||||||
});
|
});
|
||||||
syncOpen = false;
|
syncOpen = false;
|
||||||
syncCompany = null;
|
syncCompany = null;
|
||||||
syncWpCategoriesFile = null;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = failureMessage(err, i18n.t("flash.admin.syncA1Error", { name: targetName }));
|
error = failureMessage(err, i18n.t("flash.admin.syncA1Error", { name: targetName }));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1130,39 +1126,6 @@
|
|||||||
{i18n.t("admin.users.syncA1Company", { name: syncCompany.name })}
|
{i18n.t("admin.users.syncA1Company", { name: syncCompany.name })}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="space-y-2">
|
|
||||||
<label class="block text-sm font-medium text-foreground" for="sync-a1-wp-categories">
|
|
||||||
{i18n.t("admin.users.syncA1WpUpload")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="sync-a1-wp-categories"
|
|
||||||
type="file"
|
|
||||||
accept=".sql,text/plain,application/sql"
|
|
||||||
class="block w-full text-sm text-foreground file:mr-3 file:rounded-md file:border-0 file:bg-muted file:px-3 file:py-1.5 file:text-sm file:font-medium"
|
|
||||||
disabled={busy}
|
|
||||||
onchange={(e) => {
|
|
||||||
const input = e.currentTarget;
|
|
||||||
syncWpCategoriesFile = input.files?.[0] ?? null;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p class="text-xs text-muted-foreground">{i18n.t("admin.users.syncA1WpUploadHint")}</p>
|
|
||||||
{#if syncWpCategoriesFile}
|
|
||||||
<div class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
||||||
<span>{syncWpCategoriesFile.name}</span>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={busy}
|
|
||||||
onclick={() => {
|
|
||||||
syncWpCategoriesFile = null;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i18n.t("admin.users.syncA1WpUploadClear")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{i18n.t("admin.users.syncA1Warning")}
|
{i18n.t("admin.users.syncA1Warning")}
|
||||||
</p>
|
</p>
|
||||||
@@ -1174,7 +1137,6 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
syncOpen = false;
|
syncOpen = false;
|
||||||
syncCompany = null;
|
syncCompany = null;
|
||||||
syncWpCategoriesFile = null;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{i18n.t("admin.users.syncA1Cancel")}
|
{i18n.t("admin.users.syncA1Cancel")}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
Mail,
|
Mail,
|
||||||
Bell,
|
Bell,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
Save,
|
Save,
|
||||||
Trash2,
|
Trash2,
|
||||||
User as UserIcon,
|
User as UserIcon,
|
||||||
@@ -196,6 +197,11 @@
|
|||||||
let createKeyOpen = $state(false);
|
let createKeyOpen = $state(false);
|
||||||
let apiKeyName = $state("");
|
let apiKeyName = $state("");
|
||||||
let newApiKey = $state<string | null>(null);
|
let newApiKey = $state<string | null>(null);
|
||||||
|
/** Full secrets shown after create — kept in-session only so they stay copyable until refresh. */
|
||||||
|
let createdApiKeySecrets = $state<Record<string, string>>({});
|
||||||
|
let renameKeyOpen = $state(false);
|
||||||
|
let renameKeyId = $state<string | null>(null);
|
||||||
|
let renameKeyName = $state("");
|
||||||
let settingsAbort: AbortController | null = null;
|
let settingsAbort: AbortController | null = null;
|
||||||
let alertPrefs = $state<AlertPrefs>(readAlertPrefs());
|
let alertPrefs = $state<AlertPrefs>(readAlertPrefs());
|
||||||
let currentPassword = $state("");
|
let currentPassword = $state("");
|
||||||
@@ -704,6 +710,9 @@
|
|||||||
});
|
});
|
||||||
apiKeys = [created, ...apiKeys];
|
apiKeys = [created, ...apiKeys];
|
||||||
newApiKey = created.key ?? null;
|
newApiKey = created.key ?? null;
|
||||||
|
if (created.id && created.key) {
|
||||||
|
createdApiKeySecrets = { ...createdApiKeySecrets, [created.id]: created.key };
|
||||||
|
}
|
||||||
apiKeyName = "";
|
apiKeyName = "";
|
||||||
trackEvent("api_key_created");
|
trackEvent("api_key_created");
|
||||||
success = newApiKey
|
success = newApiKey
|
||||||
@@ -717,6 +726,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRenameApiKey(key: ApiKey) {
|
||||||
|
renameKeyId = key.id;
|
||||||
|
renameKeyName = (key.name ?? "").trim();
|
||||||
|
clearFormFeedback();
|
||||||
|
renameKeyOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameApiKey() {
|
||||||
|
if (!canAdmin || !planCapabilities.can("settings.api_keys")) {
|
||||||
|
error = i18n.t("settings.apiKeysAdminOnly");
|
||||||
|
notifyError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const id = renameKeyId;
|
||||||
|
const name = renameKeyName.trim();
|
||||||
|
if (!id) return;
|
||||||
|
if (!name) {
|
||||||
|
error = i18n.t("flash.settings.apiKeyNameRequired");
|
||||||
|
fieldErrors = { apiKeyName: error };
|
||||||
|
notifyError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saving = true;
|
||||||
|
clearFormFeedback();
|
||||||
|
try {
|
||||||
|
const updated = await api<ApiKey>(`/api/api-keys/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: { name }
|
||||||
|
});
|
||||||
|
apiKeys = apiKeys.map((k) => (k.id === id ? { ...k, ...updated, name: updated.name ?? name } : k));
|
||||||
|
renameKeyOpen = false;
|
||||||
|
renameKeyId = null;
|
||||||
|
renameKeyName = "";
|
||||||
|
success = i18n.t("settings.apiKeyRenamed");
|
||||||
|
notifySuccess(success);
|
||||||
|
} catch (err) {
|
||||||
|
error = applyApiFormError(err, i18n.t("flash.settings.apiKeyRenameFailed"), "rename API keys");
|
||||||
|
} finally {
|
||||||
|
saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function revokeApiKey(keyId: string) {
|
async function revokeApiKey(keyId: string) {
|
||||||
if (!confirm(i18n.t("confirm.revokeApiKey"))) {
|
if (!confirm(i18n.t("confirm.revokeApiKey"))) {
|
||||||
return;
|
return;
|
||||||
@@ -726,6 +777,8 @@
|
|||||||
try {
|
try {
|
||||||
await api(`/api/api-keys/${keyId}`, { method: "DELETE" });
|
await api(`/api/api-keys/${keyId}`, { method: "DELETE" });
|
||||||
apiKeys = apiKeys.filter((k) => k.id !== keyId);
|
apiKeys = apiKeys.filter((k) => k.id !== keyId);
|
||||||
|
const { [keyId]: _removed, ...rest } = createdApiKeySecrets;
|
||||||
|
createdApiKeySecrets = rest;
|
||||||
success = i18n.t("flash.settings.apiKeyRevoked");
|
success = i18n.t("flash.settings.apiKeyRevoked");
|
||||||
notifySuccess(success);
|
notifySuccess(success);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -735,6 +788,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionSecretFor(keyId: string): string | null {
|
||||||
|
return createdApiKeySecrets[keyId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function transferOwnership(member: TeamMember) {
|
async function transferOwnership(member: TeamMember) {
|
||||||
const userId = member.user_id ?? member.id;
|
const userId = member.user_id ?? member.id;
|
||||||
@@ -1419,6 +1476,7 @@
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell stickyRight>
|
<TableCell stickyRight>
|
||||||
{#if canAdmin}
|
{#if canAdmin}
|
||||||
|
{@const sessionSecret = sessionSecretFor(key.id)}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<DropdownMenu class="min-w-[10rem]">
|
<DropdownMenu class="min-w-[10rem]">
|
||||||
{#snippet trigger({ open, toggle })}
|
{#snippet trigger({ open, toggle })}
|
||||||
@@ -1434,6 +1492,20 @@
|
|||||||
<MoreVertical class="h-4 w-4" />
|
<MoreVertical class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
<DropdownMenuItem onclick={() => openRenameApiKey(key)}>
|
||||||
|
<Pencil class="mr-2 h-4 w-4" />
|
||||||
|
{i18n.t("settings.renameApiKey")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{#if sessionSecret}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onclick={() =>
|
||||||
|
copyText(sessionSecret, i18n.t("settings.apiKeyCopied"))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Copy class="mr-2 h-4 w-4" />
|
||||||
|
{i18n.t("settings.copyApiKey")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{:else}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
disabled
|
disabled
|
||||||
class="text-muted-foreground"
|
class="text-muted-foreground"
|
||||||
@@ -1442,6 +1514,7 @@
|
|||||||
<Copy class="mr-2 h-4 w-4" />
|
<Copy class="mr-2 h-4 w-4" />
|
||||||
{i18n.t("settings.copyUnavailable")}
|
{i18n.t("settings.copyUnavailable")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{/if}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
class="text-destructive focus:text-destructive"
|
class="text-destructive focus:text-destructive"
|
||||||
onclick={() => revokeApiKey(key.id)}
|
onclick={() => revokeApiKey(key.id)}
|
||||||
@@ -1756,7 +1829,16 @@
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label>{i18n.t("settings.apiKeyLabel")}</Label>
|
<Label>{i18n.t("settings.apiKeyLabel")}</Label>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Input value={newApiKey} readonly autocomplete="off" spellcheck={false} class="font-mono text-sm" aria-label={i18n.t("settings.newApiKeyAria")} />
|
<Input
|
||||||
|
value={newApiKey}
|
||||||
|
readonly
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck={false}
|
||||||
|
class="font-mono text-sm"
|
||||||
|
aria-label={i18n.t("settings.newApiKeyAria")}
|
||||||
|
onclick={(e) => (e.currentTarget as HTMLInputElement).select()}
|
||||||
|
onfocus={(e) => (e.currentTarget as HTMLInputElement).select()}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -1822,4 +1904,52 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Rename API Key Dialog -->
|
||||||
|
<Dialog
|
||||||
|
bind:open={renameKeyOpen}
|
||||||
|
title={i18n.t("settings.renameApiKeyTitle")}
|
||||||
|
description={i18n.t("settings.renameApiKeyHelp")}
|
||||||
|
class="sm:max-w-md sm:min-w-0"
|
||||||
|
onClose={() => {
|
||||||
|
renameKeyId = null;
|
||||||
|
renameKeyName = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="renameApiKeyName">{i18n.t("settings.keyName")}</Label>
|
||||||
|
<Input
|
||||||
|
id="renameApiKeyName"
|
||||||
|
placeholder={i18n.t("settings.keyNamePlaceholder")}
|
||||||
|
bind:value={renameKeyName}
|
||||||
|
disabled={saving}
|
||||||
|
aria-invalid={fieldInvalid(fieldErrors, "apiKeyName")}
|
||||||
|
aria-describedby={fieldDescribedBy(fieldErrors, "apiKeyName", SETTINGS_ERROR_ID)}
|
||||||
|
/>
|
||||||
|
{#if fieldErrors.apiKeyName}
|
||||||
|
<p class="text-sm text-destructive">{fieldErrors.apiKeyName}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#snippet footer()}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={saving}
|
||||||
|
onclick={() => {
|
||||||
|
renameKeyOpen = false;
|
||||||
|
renameKeyId = null;
|
||||||
|
renameKeyName = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i18n.t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={saving || !renameKeyName.trim()}
|
||||||
|
loading={saving}
|
||||||
|
onclick={renameApiKey}
|
||||||
|
>
|
||||||
|
{i18n.t("settings.saveRename")}
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
A1 Slovenija tenant seed (Postgres COPY archive, gzipped)
|
A1 Slovenija tenant seed (Postgres COPY archive, gzipped)
|
||||||
|
|
||||||
File: a1-demo-data.sql.gz
|
File: a1-demo-data.sql.gz
|
||||||
Company id: 604f23a8-b66e-4b21-8b45-0d72b68f4790
|
Company id: 604f23a8-b66e-4b21-8b45-0d72b68f4790
|
||||||
@@ -57,14 +57,17 @@ Who sees what:
|
|||||||
(switch/impersonate to a1-primary to act on A1; do not use demo API key for A1)
|
(switch/impersonate to a1-primary to act on A1; do not use demo API key for A1)
|
||||||
a1-primary@descrybe.local � A1 member only (same A1 catalog; no other tenants)
|
a1-primary@descrybe.local � A1 member only (same A1 catalog; no other tenants)
|
||||||
|
|
||||||
Category prompts overlay (committed JSON from legacy wp_product_categories):
|
Category prompts overlay (committed WP dump + JSON fallback):
|
||||||
a1-category-prompts.json
|
wp_product_categories.sql — primary Sync A1 / repair source of truth
|
||||||
|
a1-category-prompts.json — JSON fallback when WP SQL is absent
|
||||||
Applied automatically after every `npm run seed:a1` reimport (match by normalized
|
Applied automatically after every `npm run seed:a1` reimport (match by normalized
|
||||||
category name; rewrites {""OPIS IZDELKA""} ? {{description}},
|
category name; rewrites {""OPIS IZDELKA""} -> {{description}},
|
||||||
{""STARO IME IZDELKA""} ? {{name}}). Skip with -skip-category-prompts, or re-apply
|
{""STARO IME IZDELKA""} -> {{name}}). Skip with -skip-category-prompts, or re-apply
|
||||||
without wipe:
|
without wipe:
|
||||||
|
|
||||||
go run ./cmd/seed-a1 -mode apply-category-prompts -file ../../scripts/seed/a1-demo-data.sql.gz
|
go run ./cmd/seed-a1 -mode apply-category-prompts -file ../../scripts/seed/a1-demo-data.sql.gz
|
||||||
|
go run ./cmd/repair-category-prompts -apply
|
||||||
|
# uses scripts/seed/wp_product_categories.sql by default (SEED_A1_WP_CATEGORIES overrides)
|
||||||
|
|
||||||
Normal use (loads DATABASE_URL from repo-root .env if unset):
|
Normal use (loads DATABASE_URL from repo-root .env if unset):
|
||||||
npm run seed:a1 # wipe A1-only rows, import archive, prompts + mapped category backfill
|
npm run seed:a1 # wipe A1-only rows, import archive, prompts + mapped category backfill
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `wp_product_categories` (`Name`, `Prompt`) VALUES
|
||||||
|
('Monitorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"SD / HD / Full HD / 2K / 4K / 8K\"\", \"\"Stopnja osveževanja v Hz\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"diagonala v inčih in označeno z \'.\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Namizni računalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"tip procesorja\"\", \"\"tip grafične kartice\"\", \"\"pomnilnik\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez procesorja, brez grafične kartice, brez pomnilnika in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez procesorja, brez grafične kartice in brez pomnilnika}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Slušalke', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Televizorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"SD / HD / FHD / UHD / 4K / 8K\"\", \"\"TV\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"diagonala v inčih in označeno z \'.\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Projektorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Dodatki za televizorje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Kamere in fotoaparati', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Soundbar zvočniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Nosilci za TV', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Gaming monitorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"SD / HD / Full HD / 2K / 4K / 8K\"\", \"\"monitor lowercase\"\", \"\"Stopnja osveževanja v Hz\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"diagonala v inčih in označeno z \'.\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka brez diagonale in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Gaming prenosniki računalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"procesor\"\", \"\"grafična kartica\"\", \"\"pomnilnik\"\", \"\"dimenzije\"\", \"\"osveževanje (če je 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši tip izdelka sentence case in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Igralne konzole in igre', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Gaming pohištvo', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Gaming stacionarni računalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"procesor\"\", \"\"grafična kartica\"\", \"\"pomnilnik\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši tip izdelka sentence case in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Ostali gaming dodatki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Digitalna peresa', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Miške', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Mrežna oprema', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Obnovljeni prenosniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"procesor\"\", \"\"grafična kartica\"\", \"\"pomnilnik\"\", \"\"dimenzije\"\", \"\"osveževanje (če je 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši tip izdelka sentence case in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Prenosni računalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\r\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\r\n\r\nUporabi spodnjo GPT predlogo. \r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"procesor\"\", \"\"grafična kartica\"\", \"\"pomnilnik\"\", \"\"velikost zaslona v inčih - (primer 13.6\'\')\"\", \"\"osveževanje (če je 144Hz)\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši tip izdelka sentence case in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>\r\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Tablični računalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"SD / HD / FHD / UHD / 4K / 8K\"\", \"\"velikost pomnilnika\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši tip izdelka sentence case in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p>\n<H2>{Napiši naslov, ki izpostavlja en benefit in VKLJUČI tudi tip izdelka lowercase.}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Tiskalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Dodatna oprema za računalništvo', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Bluetooth zvočniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"barva v eni slovenski besedi\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Droni', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Ovitki in torbe za prenosnike', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"dimenzije\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez dimenzije in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Računalniško pohištvo', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Tipkovnice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Žepni bralniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Avto polnilniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"vrsta priključka (npr. USB C)\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Dodatki za ure', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic. Ne vključuj barve v ime.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Obnovljeni telefoni', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"velikost pomnilnika\"\", barva v eni slovenski besedi\"\", \"\"obnovljen\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in brez podatka o obnovljenosti in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve in brez podatka o obnovljenosti}</p>\n<H2>{Napiši Novo ime izdelka brez barve in brez podatka o obnovljenosti in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve in brez podatka o obnovljenosti}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve in brez podatka o obnovljenosti}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Ovitki in mape', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"model naprave za katero je namenjen\"\", barva v eni besedi\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi tip izdelka lowercase}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Zaščitna stekla', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka izdelka\"\", \"\"znamka in model naprave, za katere je namenjen\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Brezžični polnilniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Hišni polnilniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Mobilne naprave', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"velikost pomnilnika\"\", \"\"barva v eni slovenski besedi\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Ostali dodatki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka (lahko tudi brez, če ni primernega izraza) sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"barva v eni slovenski besedi (če je možnih več barv)\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Prenosne baterije', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"moč polnjenja\"\", \"\"model\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Hladilniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Štedilniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Kuhalne plošče', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pralni stroji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"kapaciteta pranja [kg]\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez kapacitete pranja in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez kapacitete pranja}</p>\n<H2>{Napiši Novo ime izdelka brez kapacitete pranja in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez kapacitete pranja}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez kapacitete pranja}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Sušilni stroji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pomivalni stroji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"širina [cm]\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez širine in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez širine}</p>\n<H2>{Napiši Novo ime izdelka brez širine in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez širine}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez širine}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pralno - sušilni stroji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Kuhinjske nape', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"širina [cm]\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez širine in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez širine}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pečice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Zamrzovalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Kavni aparati', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Likalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Mikrovalovne pečice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Ostali aparati', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Robotski sesalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Kuhinjski roboti', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Mešalniki in blenderji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Namizni žari in opekači', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Posode za kuhanje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Sesalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Električni skiroji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Dodatna oprema', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Klasična kolesa', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pametne ure', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"barva v eni slovenski besedi\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Supi', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Zimski športi', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Električna kolesa', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Fitnes oprema', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Pametne čelade', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"barva v eni slovenski besedi\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p>\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Skiroji in rolke', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"barva v eni slovenski besedi (če je več barvnih variant)\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez barve}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Športi z žogo', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Agregati', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Bazeni', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"dimenzije [cm]\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez dimenzije in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez dimenzije}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Klimatske naprave', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Naprave za ogrevanje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Parni čistilci', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Visokotlačni čistilci', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Vrtno orodje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Avto oprema', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Čistilci oken in tal', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Masažni bazeni', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Orodje za mojstre', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Sončne celice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Vlažilniki in čistilci zraka', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Žari', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Avtosedeži in lupinice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\", \"\"skupina po teži [kg]\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka brez skupine po teži in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez skupne teže}</p>\n<H2>{Napiši Novo ime izdelka brez skupne teže in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka brez skupne teže}</p>\n<H2>{Napiši Novo ime izdelka brez skupne teže in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Igrače na daljinsko vodenje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Otroška vozila', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Stolčki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Trampolini', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic. Ne vključuj barve v ime.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Družabne igre', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"ime igre\"\", \"\"model\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Igrače za najmlajše', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Poganjalčki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Tobogani in gugalnice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Vozički', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Brivniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Električne zobne ščetke', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Osebna nega', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Zobne prhe', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Depilatorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Oblikovalci in sušilniki za lase', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Strižniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Nadzorne kamere', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"tip izdelka lowercase\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Vtičnice', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"vrsta vtičnice (npr. RJ45)\"\", \"\"tip izdelka lowercase\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Senzorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\n\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\n\nUporabi spodnjo GPT predlogo. \n\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\n\nGPT predloga:\n\n\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka sentence case\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\n\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>'),
|
||||||
|
('Dodatki za dom', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\r\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\r\n\r\nUporabi spodnjo GPT predlogo. \r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"\"tip izdelka lowercase\"\", \"\"znamka s pravilno kapitalizacijo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE VKLJUČI Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>\r\n'),
|
||||||
|
('Klimatske naprave', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"\"OPIS IZDELKA\"\"};\r\nStaro_ime_izdelka: {\"\"STARO IME IZDELKA\"\"};\r\n\r\nUporabi spodnjo GPT predlogo. \r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"\"znamka s pravilno kapitalizacijo\"\", \"\"klima z montažo\"\", \"\"\"poln model izdelka, če lahko z besedo in ID uppercase\"\"\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit, in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI tudi Novo ime izdelka.}</p>\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p><b>{Tehnične specifikacije}</b><ul><li>{Napiši 3-10 tehničnih specifikacij v alinejah}</li></ul>\r\n'),
|
||||||
|
('Kabli', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo. \r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"vrsta kabla (UTP/STP…)\", \"kabel\", \"dolžina [m]\", \"znamka normal case\", \"model izdelka uppercase\". Če je kabel za polnjenje, potem sestavi ime tako: \"polnilni kabel\", \"znamka normal case\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H1>{Zapiši Novo_ime_izdelka}</H1>\r\n<H2>{Naj vključuje Novo_ime_izdelka in izpostavi en benefit}</H2>\r\n<p>{Nadaljuj iz naslova in napiši 100 besed o temu benefitu, ne vključuj Novo_ime_izdelka}</p>'),
|
||||||
|
('Kamp oprema in aktivnosti na prostem', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"vrsta izdelka lowercase\", \"znamka s pravilno kapitalizacijo\", \"model izdelka\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka}</p>\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka}</p>\r\n<H2>{Napiši Novo ime izdelka in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed, ki NE vsebuje Novo ime izdelka.}</p>\r\n\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Cvrtniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nGPT predloga:\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"znamka s pravilno kapitalizacijo\", \"cvrtnik lowercase\", \"model izdelka uppercase\", \"kapaciteta v litrih\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez kapacitete}</p>\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez kapacitete}</p>\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Grelniki vode', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nGPT predloga:\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"znamka s pravilno kapitalizacijo\", \"grelnik vode lowercase\", \"model izdelka uppercase\", \"kapaciteta v litrih\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez kapacitete}</p>\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez kapacitete}</p>\r\n<H2>{Napiši Novo ime izdelka brez kapacitete in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Otroške pametne ure', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nGPT predloga:\r\n\r\n<name>{Napiši novo ime izdelka po formuli: \"znamka s pravilno kapitalizacijo\", \"otroška pametna ura lowercase\", \"model izdelka uppercase\", \"barva\". Ne uporabljaj vejic.}</name>\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n\r\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez barve}</p>\r\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka brez barve}</p>\r\n<H2>{Napiši Novo ime izdelka brez barve in izpostavi en benefit}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Postelje', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"»postelja«\", \"znamka s pravilno kapitalizacijo\", \"model izdelka\", \"velikost v cm (npr. 120x200cm)\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Napiši material postelje (npr. »Lesena postelja« ali »Kovinska postelja«) in izpostavi en benefit ter VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in NE napiši Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Potrošni material', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"tip izdelka lowercase\", \"znamka s pravilno kapitalizacijo\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>\r\n'),
|
||||||
|
('Uničevalci dokumentov', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"»Uničevalec dokumentov«\", \"znamka s pravilno kapitalizacijo\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>'),
|
||||||
|
('Plastifikatorji', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"»Plastifikator«\", \"znamka s pravilno kapitalizacijo\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>'),
|
||||||
|
('Giljotine in rezalniki', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"tip izdelka (»Giljotina« ali »Rezalnik«)\", \"znamka s pravilno kapitalizacijo\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>'),
|
||||||
|
('Tiskalniki nalepk in trakov', 'Ustvari nov opis izdelka v Slovenščini z naslednjimi spremenljivkami:\r\n\r\nStar_opis_izdelka: {\"OPIS IZDELKA\"};\r\nStaro_ime_izdelka: {\"STARO IME IZDELKA\"};\r\n\r\nUporabi spodnjo GPT predlogo.\r\n\r\nSledi tej GPT predlogi stavek po stavek in sestavi nov opis izdelka:\r\n\r\nGPT predloga:\r\n<name>{Napiši novo ime izdelka po formuli: \"»Tiskalnik nalepk«\", \"znamka s pravilno kapitalizacijo\", \"model izdelka uppercase\". Ne uporabljaj vejic.}</name>\r\n\r\n<metaDescription>{Najprej napiši Novo_ime_izdelka in potem nadaljuj dokler nisi zapisal 140 znakov vključno s presledki}</metaDescription>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in NE vsebuje Novo ime izdelka.}</p>\r\n<H2>{Izpostavi en benefit in VKLJUČI Novo ime izdelka}</H2>\r\n<p>{Napiši odstavek, ki je dolg 100 besed in VKLJUČI Novo ime izdelka.}</p>\r\n<b>Tehnične specifikacije</b>\r\n<ul>\r\n<li>{Napiši 3–10 tehničnih specifikacij v alinejah}</li>\r\n</ul>');
|
||||||
|
-- 2026-08-06 15:41:26 UTC
|
||||||
Reference in New Issue
Block a user