← back to Designer Wallcoverings
DW-Agents/dw-agents/koroseal-fixed-import.ts
314 lines
/**
* FIXED Koroseal Import - Images, Specs, Tags
* Removed invalid 'variants' field from ProductInput
*/
import { chromium } from 'playwright';
import Anthropic from '@anthropic-ai/sdk';
const SHOPIFY_STORE = 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const anthropic = new Anthropic();
interface Product {
sku: string;
color: string;
collection: string;
collectionUrl: string;
image?: string;
specs?: any;
tags?: string[];
}
// Collections to import with their URLs
const COLLECTIONS = [
{ name: 'adalyn-weave', url: 'https://koroseal.com/products/wallcoverings/adalyn-weave', type: 'wallcovering' },
{ name: 'chateau', url: 'https://koroseal.com/products/wallcoverings/chateau', type: 'wallcovering' },
{ name: 'spire', url: 'https://koroseal.com/products/wallcoverings/spire', type: 'wallcovering' },
{ name: 'muga-silk', url: 'https://koroseal.com/products/wallcoverings/muga-silk', type: 'wallcovering' },
{ name: 'color-wash-cork', url: 'https://koroseal.com/products/specialty/koroseal-specialty/color-wash-cork', type: 'specialty' },
{ name: 'woven-wood', url: 'https://koroseal.com/products/specialty/koroseal-specialty/woven-wood', type: 'specialty' },
{ name: 'quercus', url: 'https://koroseal.com/products/specialty/koroseal-specialty/quercus', type: 'specialty' },
{ name: 'kilika', url: 'https://koroseal.com/products/specialty/koroseal-specialty/kilika', type: 'specialty' },
{ name: 'ruche', url: 'https://koroseal.com/products/specialty/koroseal-specialty/ruche', type: 'specialty' },
];
function generateDWWC(): string {
const ts = Date.now().toString().slice(-10);
const rand = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
return `DWWC-KOR-${ts}-${rand}`;
}
async function scrapeCollection(collectionUrl: string): Promise<{ products: Product[], specs: any }> {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const products: Product[] = [];
let specs: any = {};
try {
await page.goto(collectionUrl, { waitUntil: 'load', timeout: 45000 });
await page.waitForTimeout(4000);
// Get colorways with images
const colorways = await page.evaluate(() => {
const results: any[] = [];
document.querySelectorAll('.thumbscontain a.thumbprev').forEach((el) => {
const link = el as HTMLAnchorElement;
const skuEl = el.querySelector('span.tptext');
const skuText = skuEl?.childNodes[0]?.textContent?.trim() || '';
const colorEl = el.querySelector('span.tptext em');
const colorName = colorEl?.textContent?.trim() || '';
const picEl = el.querySelector('span.tppic') as HTMLElement;
const bgImage = picEl?.style.backgroundImage || '';
const imageMatch = bgImage.match(/url\(['"]?([^'"]+)['"]?\)/);
const image = imageMatch ? imageMatch[1] : '';
if (skuText) {
results.push({
sku: skuText.toUpperCase(),
color: colorName,
image: image
});
}
});
return results;
});
// Get specs from page
specs = await page.evaluate(() => {
const allText = document.body.innerText;
const result: any = { pdfs: [] };
const patterns: [string, RegExp][] = [
['material', /Material\s*:\s*([^\n]+)/i],
['backing', /Backing\s*:\s*([^\n]+)/i],
['fireRating', /Fire Rating\s*:\s*([^\n]+)/i],
['rollWidth', /Roll Width[^:]*:\s*([^\n]+)/i],
['patternMatch', /Pattern Match\s*:\s*([^\n]+)/i],
['verticalRepeat', /Vertical Repeat\s*:\s*([^\n]+)/i],
];
for (const [key, regex] of patterns) {
const match = allText.match(regex);
if (match) result[key] = match[1].trim();
}
// Get PDFs
document.querySelectorAll('a[href*=".pdf"]').forEach((el) => {
const link = el as HTMLAnchorElement;
const name = el.textContent?.trim() || 'Document';
if (!result.pdfs.some((p: any) => p.url === link.href)) {
result.pdfs.push({ name, url: link.href });
}
});
return result;
});
const collectionName = collectionUrl.split('/').pop() || '';
for (const c of colorways) {
products.push({
...c,
collection: collectionName,
collectionUrl
});
}
} catch (error) {
console.log(` Error: ${(error as Error).message}`);
}
await browser.close();
return { products, specs };
}
async function analyzeImageForTags(imageUrl: string): Promise<string[]> {
try {
const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.arrayBuffer();
const base64Image = Buffer.from(imageBuffer).toString('base64');
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 500,
messages: [{
role: 'user',
content: [
{
type: 'image',
source: { type: 'base64', media_type: 'image/jpeg', data: base64Image }
},
{
type: 'text',
text: `Analyze this wallcovering image and return ONLY a comma-separated list of interior design tags. Include:
- Background color (e.g., "Background Color Teal")
- 2-3 color names
- Style (Contemporary, Traditional, etc.)
- Pattern type (Textured, Woven, etc.)
- Material look (Silk-Look, Natural Fiber, etc.)
Return ONLY the comma-separated tags, nothing else.`
}
]
}]
});
const text = response.content[0];
if (text && 'text' in text) {
return text.text.split(',').map(t => t.trim()).filter(Boolean);
}
} catch (error) {
console.log(` Tag error: ${(error as Error).message}`);
}
return [];
}
async function createProduct(product: Product, specs: any, collectionType: string): Promise<boolean> {
const dwwc = generateDWWC();
const collectionName = product.collection.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
const title = `${product.color} ${collectionName} Wallcovering (${product.sku})`;
const productType = collectionType === 'specialty' ? 'Specialty Wallcovering' : 'Type II Wallcovering';
// Build description with specs
let description = `<h2>${collectionName} Collection</h2>
<p><strong>SKU:</strong> ${product.sku}</p>
<p><strong>DWWC:</strong> ${dwwc}</p>
<p><strong>Color:</strong> ${product.color}</p>
<p><strong>Vendor:</strong> Koroseal</p>`;
if (specs && Object.keys(specs).length > 0) {
description += `<h3>Specifications</h3><ul>`;
if (specs.material) description += `<li><strong>Material:</strong> ${specs.material}</li>`;
if (specs.backing) description += `<li><strong>Backing:</strong> ${specs.backing}</li>`;
if (specs.fireRating) description += `<li><strong>Fire Rating:</strong> ${specs.fireRating}</li>`;
if (specs.rollWidth) description += `<li><strong>Roll Width:</strong> ${specs.rollWidth}</li>`;
if (specs.patternMatch) description += `<li><strong>Pattern Match:</strong> ${specs.patternMatch}</li>`;
if (specs.verticalRepeat) description += `<li><strong>Vertical Repeat:</strong> ${specs.verticalRepeat}</li>`;
description += `</ul>`;
if (specs.pdfs && specs.pdfs.length > 0) {
description += `<h3>Documentation</h3><ul>`;
specs.pdfs.slice(0, 5).forEach((pdf: any) => {
description += `<li><a href="${pdf.url}" target="_blank">${pdf.name}</a></li>`;
});
description += `</ul>`;
}
}
// Build tags
const baseTags = [
'Koroseal',
collectionName,
product.color,
product.sku,
dwwc,
productType,
'Commercial Grade'
];
// Get AI-generated tags if image available
let aiTags: string[] = [];
if (product.image) {
aiTags = await analyzeImageForTags(product.image);
}
const allTags = [...new Set([...baseTags, ...aiTags])];
// FIXED: Removed variants from ProductInput - not valid in Shopify GraphQL
const mutation = `
mutation productCreate($input: ProductInput!, $media: [CreateMediaInput!]) {
productCreate(input: $input, media: $media) {
product { id title }
userErrors { field message }
}
}
`;
const variables: any = {
input: {
title,
descriptionHtml: description,
vendor: 'Koroseal',
productType,
tags: allTags,
status: 'DRAFT'
}
};
// Add image if available
if (product.image) {
variables.media = [{
originalSource: product.image,
alt: title,
mediaContentType: 'IMAGE'
}];
}
try {
const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/2024-01/graphql.json`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': SHOPIFY_TOKEN,
},
body: JSON.stringify({ query: mutation, variables }),
});
const data = await response.json();
if (data.data?.productCreate?.product?.id) {
const id = data.data.productCreate.product.id.split('/').pop();
console.log(` ✓ Created ${id} with ${aiTags.length} AI tags`);
return true;
}
if (data.data?.productCreate?.userErrors?.length > 0) {
console.log(` ✗ ${data.data.productCreate.userErrors[0].message}`);
} else if (data.errors) {
console.log(` ✗ ${data.errors[0]?.message || 'API Error'}`);
}
return false;
} catch (error) {
console.log(` ✗ ${(error as Error).message}`);
return false;
}
}
async function main() {
console.log('=== KOROSEAL IMPORT (FIXED) ===');
console.log('Images + Specs + Interior Designer Tags + DWWC Numbers\n');
let totalCreated = 0;
let totalFailed = 0;
for (const collection of COLLECTIONS) {
console.log(`\n📁 ${collection.name.toUpperCase()}`);
console.log(` Scraping ${collection.url}...`);
const { products, specs } = await scrapeCollection(collection.url);
console.log(` Found ${products.length} colorways`);
if (specs.material) {
console.log(` Specs: ${specs.material}, ${specs.backing || 'N/A'}`);
}
for (let i = 0; i < products.length; i++) {
const product = products[i];
console.log(` [${i + 1}/${products.length}] ${product.sku} - ${product.color}`);
console.log(` Image: ${product.image ? '✓' : '✗'}`);
const success = await createProduct(product, specs, collection.type);
if (success) totalCreated++;
else totalFailed++;
// Rate limiting
await new Promise(r => setTimeout(r, 1000));
}
}
console.log('\n=== IMPORT COMPLETE ===');
console.log(`Created: ${totalCreated}`);
console.log(`Failed: ${totalFailed}`);
}
main().catch(console.error);