anycoder-048565f9 / index.html
eubottura's picture
Upload folder using huggingface_hub
8b3e50b verified
<!DOCTYPE html>
<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>
&copy; 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>