Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Variable Extraction & Template Injection Specialist</title> | |
| <!-- Importing FontAwesome for Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #4f46e5; | |
| --primary-hover: #4338ca; | |
| --secondary: #64748b; | |
| --bg-body: #f1f5f9; | |
| --bg-card: #ffffff; | |
| --text-main: #0f172a; | |
| --text-muted: #64748b; | |
| --border: #e2e8f0; | |
| --success: #10b981; | |
| --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | |
| --radius: 12px; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| } | |
| body { | |
| background-color: var(--bg-body); | |
| color: var(--text-main); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| header { | |
| background-color: var(--bg-card); | |
| border-bottom: 1px solid var(--border); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| .brand i { | |
| font-size: 1.5rem; | |
| } | |
| .anycoder-link { | |
| font-size: 0.875rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.2s; | |
| font-weight: 500; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--primary); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| width: 100%; | |
| padding: 2rem; | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 2rem; | |
| } | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Cards */ | |
| .card { | |
| background: var(--bg-card); | |
| border-radius: var(--radius); | |
| box-shadow: var(--shadow); | |
| padding: 1.5rem; | |
| border: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .card-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| border-bottom: 1px solid var(--border); | |
| padding-bottom: 1rem; | |
| } | |
| .card-header h2 { | |
| font-size: 1.125rem; | |
| font-weight: 600; | |
| color: var(--text-main); | |
| } | |
| .card-header i { | |
| color: var(--primary); | |
| } | |
| /* Form Elements */ | |
| .form-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| label { | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--text-main); | |
| } | |
| input[type="text"], | |
| textarea { | |
| padding: 0.75rem; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| font-size: 1rem; | |
| transition: border-color 0.2s, box-shadow 0.2s; | |
| width: 100%; | |
| } | |
| input[type="text"]:focus, | |
| textarea:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 150px; | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 0.9rem; | |
| } | |
| .helper-text { | |
| font-size: 0.75rem; | |
| color: var(--text-muted); | |
| } | |
| /* Buttons */ | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| padding: 0.75rem 1.5rem; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 1rem; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background-color: var(--primary-hover); | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background-color: #f8fafc; | |
| color: var(--text-muted); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-secondary:hover { | |
| background-color: #e2e8f0; | |
| color: var(--text-main); | |
| } | |
| /* Analysis Grid (Variables) */ | |
| .analysis-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .stat-box { | |
| background: #f8fafc; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| border: 1px solid var(--border); | |
| } | |
| .stat-label { | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--text-muted); | |
| margin-bottom: 0.25rem; | |
| } | |
| .stat-value { | |
| font-weight: 700; | |
| color: var(--text-main); | |
| font-size: 1rem; | |
| word-break: break-word; | |
| } | |
| /* Output Area */ | |
| .output-container { | |
| position: relative; | |
| background: #1e293b; | |
| color: #e2e8f0; | |
| padding: 1.5rem; | |
| border-radius: 8px; | |
| min-height: 200px; | |
| white-space: pre-wrap; | |
| font-family: 'Segoe UI', sans-serif; /* Readable font for output */ | |
| } | |
| .output-placeholder { | |
| color: #64748b; | |
| font-style: italic; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100%; | |
| min-height: 167px; | |
| } | |
| /* Toast Notification */ | |
| .toast-container { | |
| position: fixed; | |
| bottom: 2rem; | |
| right: 2rem; | |
| z-index: 1000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| .toast { | |
| background: var(--bg-card); | |
| color: var(--text-main); | |
| padding: 1rem 1.5rem; | |
| border-radius: 8px; | |
| box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1); | |
| border-left: 4px solid var(--primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| animation: slideIn 0.3s ease-out forwards; | |
| max-width: 350px; | |
| } | |
| .toast.success { border-left-color: var(--success); } | |
| .toast.error { border-left-color: #ef4444; } | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes fadeOut { | |
| to { opacity: 0; transform: translateX(100%); } | |
| } | |
| /* Footer */ | |
| footer { | |
| text-align: center; | |
| padding: 2rem; | |
| color: var(--text-muted); | |
| font-size: 0.875rem; | |
| border-top: 1px solid var(--border); | |
| margin-top: auto; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <i class="fa-solid fa-robot"></i> | |
| <span>Template Specialist</span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-external-link-alt" style="font-size: 0.75rem;"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <!-- INPUT SECTION --> | |
| <section class="card"> | |
| <div class="card-header"> | |
| <i class="fa-solid fa-pen-to-square"></i> | |
| <h2>Input Data</h2> | |
| </div> | |
| <div class="form-group"> | |
| <label for="inputProductName">INPUT_PRODUCT_NAME</label> | |
| <input type="text" id="inputProductName" placeholder="e.g. Camiseta Básica Masculina de Algodão Pack de 3"> | |
| <span class="helper-text">Enter the full product title containing material, gender, and quantity info.</span> | |
| </div> | |
| <div class="form-group"> | |
| <label for="inputColors">INPUT_COLORS</label> | |
| <input type="text" id="inputColors" placeholder="e.g. Preto, Branco, Cinza, Azul Marinho"> | |
| <span class="helper-text">Enter colors separated by commas.</span> | |
| </div> | |
| <div class="form-group"> | |
| <label for="baseTemplate">BASE TEMPLATE</label> | |
| <textarea id="baseTemplate" placeholder="Paste your base template here...">Leve 5 e pague 4. This {PRODUCT_NAME} is crafted from high-quality {MATERIAL}. Designed for the modern {TARGET_GENDER_LOWER}, it features a comfortable fit. Available in stunning colors: {COLOR_LIST}. Includes {TOTAL_QUANTITY} pairs in this bundle. Perfect for the {TARGET_GENDER_LOWER} looking for style. The female model is wearing size M.</textarea> | |
| <span class="helper-text">Variables: {PRODUCT_NAME}, {MATERIAL}, {COLOR_LIST}, {COLOR}, {TOTAL_QUANTITY}, {TARGET_GENDER_LOWER}.</span> | |
| </div> | |
| <button class="btn btn-primary" id="generateBtn"> | |
| <i class="fa-solid fa-bolt"></i> Execute Injection | |
| </button> | |
| </section> | |
| <!-- OUTPUT SECTION --> | |
| <section class="card"> | |
| <div class="card-header"> | |
| <i class="fa-solid fa-microchip"></i> | |
| <h2>Analysis & Output</h2> | |
| </div> | |
| <!-- Variable Breakdown --> | |
| <div class="analysis-grid"> | |
| <div class="stat-box"> | |
| <div class="stat-label">Product Name</div> | |
| <div class="stat-value" id="resProductName">-</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Material (EN)</div> | |
| <div class="stat-value" id="resMaterial">-</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Target Audience</div> | |
| <div class="stat-value" id="resAudience">-</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Total Quantity</div> | |
| <div class="stat-value" id="resQuantity">-</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Primary Color</div> | |
| <div class="stat-value" id="resColor">-</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Giveaway</div> | |
| <div class="stat-value" id="resGiveaway">No</div> | |
| </div> | |
| </div> | |
| <!-- Final Output Text --> | |
| <div class="form-group" style="flex: 1;"> | |
| <label>Final Output Block</label> | |
| <div class="output-container" id="outputResult"> | |
| <div class="output-placeholder">Processed text will appear here...</div> | |
| </div> | |
| </div> | |
| <div style="display: flex; justify-content: flex-end;"> | |
| <button class="btn btn-secondary" id="copyBtn" disabled> | |
| <i class="fa-regular fa-copy"></i> Copy Text | |
| </button> | |
| </div> | |
| </section> | |
| </main> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <footer> | |
| © 2023 Variable Extraction Specialist. Automated Processing System. | |
| </footer> | |
| <script> | |
| // --- Configuration & Data Maps --- | |
| // Portuguese to English Material Dictionary | |
| const materialMap = { | |
| "algodão": "Cotton", | |
| "algodon": "Cotton", | |
| "linho": "Linen", | |
| "poliéster": "Polyester", | |
| "poliester": "Polyester", | |
| "seda": "Silk", | |
| "jeans": "Denim", | |
| "denim": "Denim", | |
| "couro": "Leather", | |
| "lã": "Wool", | |
| "la": "Wool", | |
| "viscose": "Viscose", | |
| "elastano": "Elastane", | |
| "spandex": "Spandex", | |
| "nylon": "Nylon" | |
| }; | |
| const maleKeywords = ["masculina", "masculino", "homem", "menino", "men's", "male"]; | |
| const femaleKeywords = ["feminina", "feminino", "mulher", "menina", "woman", "female"]; | |
| // --- DOM Elements --- | |
| const els = { | |
| prodName: document.getElementById('inputProductName'), | |
| colors: document.getElementById('inputColors'), | |
| template: document.getElementById('baseTemplate'), | |
| btnGenerate: document.getElementById('generateBtn'), | |
| btnCopy: document.getElementById('copyBtn'), | |
| output: document.getElementById('outputResult'), | |
| // Result fields | |
| resName: document.getElementById('resProductName'), | |
| resMat: document.getElementById('resMaterial'), | |
| resAud: document.getElementById('resAudience'), | |
| resQty: document.getElementById('resQuantity'), | |
| resCol: document.getElementById('resColor'), | |
| resGive: document.getElementById('resGiveaway'), | |
| toastContainer: document.getElementById('toastContainer') | |
| }; | |
| // --- Core Logic Functions --- | |
| /** | |
| * Extracts material from input string and translates to English. | |
| */ | |
| function extractMaterial(text) { | |
| const lowerText = text.toLowerCase(); | |
| for (const [pt, en] of Object.entries(materialMap)) { | |
| if (lowerText.includes(pt)) { | |
| return en; | |
| } | |
| } | |
| return "Mixed"; // Fallback if no known material found | |
| } | |
| /** | |
| * Determines target audience based on keywords. | |
| * Returns 'Male' or 'Female'. | |
| */ | |
| function determineAudience(text) { | |
| const lowerText = text.toLowerCase(); | |
| // Check for Male keywords explicitly | |
| const isMale = maleKeywords.some(keyword => lowerText.includes(keyword)); | |
| if (isMale) return 'Male'; | |
| // Check for Female keywords (optional, as default is female, but good for explicit confirmation) | |
| const isFemale = femaleKeywords.some(keyword => lowerText.includes(keyword)); | |
| if (isFemale) return 'Female'; | |
| return 'Female'; // Default per requirements | |
| } | |
| /** | |
| * Detects giveaway keywords. | |
| */ | |
| function detectGiveaway(text) { | |
| const keywords = ["brinde", "bônus", "bonus", "presente", "kit", "free", "gift"]; | |
| const lowerText = text.toLowerCase(); | |
| return keywords.some(k => lowerText.includes(k)); | |
| } | |
| /** | |
| * Calculates total quantity. | |
| * Priority 1: Explicit number in product name. | |
| * Priority 2: Count of colors provided. | |
| */ | |
| function calculateQuantity(productName, colorsArray) { | |
| // Priority 1: Scan for explicit numbers (e.g., "Pack de 3", "10 unidades", "5") | |
| // Regex looks for digits not immediately followed by letters (to avoid matching sizes like S40 ideally, though simple \d+ is often enough for qty) | |
| // Better regex: looks for patterns like "Pack de X", "X un", or just standalone numbers if context implies qty. | |
| const explicitMatch = productName.match(/(?:pack|kit|de|unidades|unidade|pcs|pairs|pares)?\s*(\d+)\s*(?:unidades|unidade|pcs|pairs|pares)?/i); | |
| if (explicitMatch && explicitMatch[1]) { | |
| return parseInt(explicitMatch[1], 10); | |
| } | |
| // Priority 2: Color count | |
| return colorsArray.length; | |
| } | |
| /** | |
| * Formats color list into a grammatically correct string (Oxford comma). | |
| */ | |
| function formatColorList(colorArray) { | |
| if (colorArray.length === 0) return ""; | |
| if (colorArray.length === 1) return colorArray[0]; | |
| if (colorArray.length === 2) return `${colorArray[0]} and ${colorArray[1]}`; | |
| // Oxford comma logic | |
| const last = colorArray[colorArray.length - 1]; | |
| const firsts = colorArray.slice(0, -1); | |
| return `${firsts.join(", ")}, and ${last}`; | |
| } | |
| /** | |
| * Cleans product name to extract core title. | |
| * (Simple heuristic: remove quantity suffixes and material suffixes if they look like tags) | |
| */ | |
| function cleanProductName(text) { | |
| let clean = text.trim(); | |
| // Remove "Pack de X" or similar quantity indicators at end or start | |
| clean = clean.replace(/^(pack|kit)\s*(de)?\s*\d+(\s*(unidades|un|pcs|pares|pairs))?/gi, ""); | |
| clean = clean.replace(/(pack|kit)\s*(de)?\s*\d+(\s*(unidades|un|pcs|pares|pairs))?$/gi, ""); | |
| // Clean up extra spaces | |
| return clean.replace(/\s+/g, " ").trim(); | |
| } | |
| /** | |
| * Performs gender-specific string replacements. | |
| */ | |
| function adaptGender(text, targetAudience) { | |
| if (targetAudience === 'Female') return text; // No changes needed | |
| let modified = text; | |
| // Map: key -> value (Case insensitive replacement logic needed or direct string replace) | |
| // We will use Regex with 'i' flag and a replacer function to preserve case if possible, | |
| // but for simplicity and strict constraint adherence, we replace specific terms. | |
| const replacements = [ | |
| { find: /female model/gi, replace: "male model" }, | |
| { find: /women's/gi, replace: "men's" }, | |
| { find: /woman/gi, replace: "man" }, | |
| { find: /feminine hand/gi, replace: "masculine hand" }, | |
| { find: /brazilian woman/gi, replace: "brazilian man" }, | |
| { find: /her/gi, replace: "his" }, // Common extra | |
| { find: /she/gi, replace: "he" } // Common extra | |
| ]; | |
| replacements.forEach(rule => { | |
| modified = modified.replace(rule.find, rule.replace); | |
| }); | |
| return modified; | |
| } | |
| // --- Main Processing Function --- | |
| function process() { | |
| // 1. Get Inputs | |
| const rawProduct = els.prodName.value.trim(); | |
| const rawColors = els.colors.value.trim(); | |
| const template = els.template.value; | |
| // Validation | |
| if (!rawProduct || !rawColors || !template) { | |
| showToast("Please fill in all fields (Product Name, Colors, and Template).", "error"); | |
| return; | |
| } | |
| try { | |
| // 2. Analyze Product Name | |
| const productName = cleanProductName(rawProduct); | |
| const material = extractMaterial(rawProduct); | |
| const audience = determineAudience(rawProduct); | |
| const isGiveaway = detectGiveaway(rawProduct); | |
| // 3. Analyze Colors | |
| // Split by comma, trim whitespace, filter empty | |
| const colorArray = rawColors.split(',').map(c => c.trim()).filter(c => c.length > 0); | |
| const colorList = formatColorList(colorArray); | |
| const primaryColor = colorArray.length > 0 ? colorArray[0] : ""; | |
| // 4. Calculate Quantity | |
| const totalQty = calculateQuantity(rawProduct, colorArray); | |
| // 5. Update Analysis UI | |
| els.resName.textContent = productName; | |
| els.resMat.textContent = material; | |
| els.resAud.textContent = audience; | |
| els.resQty.textContent = totalQty; | |
| els.resCol.textContent = primaryColor; | |
| els.resGive.textContent = isGiveaway ? "Yes" : "No"; | |
| if(isGiveaway) els.resGive.style.color = "var(--success)"; | |
| else els.resGive.style.color = "var(--text-main)"; | |
| // 6. Execute Substitutions & Logic | |
| let processedText = template; | |
| // Helper to replace all occurrences of a key | |
| const replaceVar = (key, value) => { | |
| const regex = new RegExp(`{${key}}`, 'g'); | |
| processedText = processedText.replace(regex, value); | |
| }; | |
| // Standard Variable Injection | |
| replaceVar("PRODUCT_NAME", productName); | |
| replaceVar("MATERIAL", material); | |
| replaceVar("COLOR_LIST", colorList); | |
| replaceVar("COLOR", primaryColor); | |
| replaceVar("TOTAL_QUANTITY", totalQty); | |
| // Dynamic Gender Variables | |
| replaceVar("TARGET_GENDER_LOWER", audience.toLowerCase()); | |
| replaceVar("TARGET_GENDER", audience); // Capitalized | |
| // Specific Header Logic: "Leve X" | |
| // The requirement says: "Replace phrases like 'Leve 5' in headers with 'Leve {TOTAL_QUANTITY}'" | |
| // We will look for "Leve" followed by a number and replace the number part. | |
| // Regex: Leve\s+\d+ | |
| processedText = processedText.replace(/(Leve\s+)\d+/gi, `$1${totalQty}`); | |
| // Gender Adaptation (Text Swapping) | |
| processedText = adaptGender(processedText, audience); | |
| // Giveaway Logic (Bonus Text Injection) | |
| // If giveaway detected, maybe append a specific line or replace a placeholder if it existed? | |
| // Prompt says: "Apply necessary promotional text logic if detected." | |
| // Since we don't have a specific giveaway placeholder in the generic template, | |
| // we will inject a standard phrase at the end if the template contains a {PROMO} placeholder, | |
| // or if not found, we prepend it to ensure the logic is visible. | |
| if (isGiveaway) { | |
| if (processedText.includes("{PROMO}")) { | |
| replaceVar("PROMO", "🎁 Special Bonus included in this package!"); | |
| } else { | |
| // If no placeholder, let's just append it to the end to show we did the work | |
| processedText += "\n\n🎁 Special Bonus included in this package!"; | |
| } | |
| } | |
| // 7. Final Output | |
| els.output.textContent = processedText; | |
| els.output.classList.remove('output-placeholder'); | |
| els.btnCopy.disabled = false; | |
| showToast("Text generated successfully!", "success"); | |
| } catch (error) { | |
| console.error(error); | |
| showToast("An error occurred during processing.", "error"); | |
| } | |
| } | |
| // --- Utilities --- | |
| function showToast(message, type = "success") { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| const icon = type === 'success' ? '<i class="fa-solid fa-check-circle"></i>' : '<i class="fa-solid fa-circle-exclamation"></i>'; | |
| toast.innerHTML = `${icon} <span>${message}</span>`; | |
| els.toastContainer.appendChild(toast); | |
| // Remove after 3 seconds | |
| setTimeout(() => { | |
| toast.style.animation = "fadeOut 0.3s ease-in forwards"; | |
| toast.addEventListener('animationend', () => { | |
| toast.remove(); | |
| }); | |
| }, 3000); | |
| } | |
| function copyToClipboard() { | |
| const text = els.output.textContent; | |
| if (!text) return; | |
| navigator.clipboard.writeText(text).then(() => { | |
| showToast("Copied to clipboard!", "success"); | |
| }).catch(() => { | |
| showToast("Failed to copy.", "error"); | |
| }); | |
| } | |
| // --- Event Listeners --- | |
| els.btnGenerate.addEventListener('click', process); | |
| els.btnCopy.addEventListener('click', copyToClipboard); | |
| // Optional: Allow "Enter" key in inputs to trigger generation (if desired, but risky for textareas) | |
| els.prodName.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') els.colors.focus(); | |
| }); | |
| els.colors.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') process(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |