← back to Designer Wallcoverings
security: purge leaked ANTHROPIC_API_KEY from tracked marketing-agent.ts.backup
62f26ac46bb6af392792f6c6886146cbf6c33d76 · 2026-09-17 16:48:36 -0700 · Steve
Code-review finding (2026-09-17): live sk-ant key was still present in this
tracked backup file even after ecosystem.config.cjs was env-ified. *.backup
was already gitignored (the file predated the rule). Removes the working-tree
leak. NOTE: key remains in git history — rotation in the Anthropic console is
the actual revocation and is still required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1fbVDeQRfYhz5QnxYoPU
Files touched
D DW-Agents/dw-agents/marketing-agent.ts.backup
Diff
commit 62f26ac46bb6af392792f6c6886146cbf6c33d76
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 17 16:48:36 2026 -0700
security: purge leaked ANTHROPIC_API_KEY from tracked marketing-agent.ts.backup
Code-review finding (2026-09-17): live sk-ant key was still present in this
tracked backup file even after ecosystem.config.cjs was env-ified. *.backup
was already gitignored (the file predated the rule). Removes the working-tree
leak. NOTE: key remains in git history — rotation in the Anthropic console is
the actual revocation and is still required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1fbVDeQRfYhz5QnxYoPU
---
DW-Agents/dw-agents/marketing-agent.ts.backup | 1993 -------------------------
1 file changed, 1993 deletions(-)
diff --git a/DW-Agents/dw-agents/marketing-agent.ts.backup b/DW-Agents/dw-agents/marketing-agent.ts.backup
deleted file mode 100644
index 8407d3e2..00000000
--- a/DW-Agents/dw-agents/marketing-agent.ts.backup
+++ /dev/null
@@ -1,1993 +0,0 @@
-/**
- * DW-Agents: Marketing Agent
- *
- * AI-powered marketing content creation and management:
- * 1. View and manage Shopify blog posts
- * 2. Create compelling blog posts from new SKUs
- * 3. Generate social media content (Instagram, Facebook)
- * 4. Draft email campaigns (Constant Contact integration)
- * 5. Chat interface for content creation assistance
- *
- * Port: 9881
- */
-
-import Anthropic from '@anthropic-ai/sdk';
-import express from 'express';
-import { requireAuth as ssoAuth, handleLogin, setSSOToken, SSO_TOKEN_NAME, SSO_TOKEN_VALUE } from './shared-auth.js';
-import cookieParser from 'cookie-parser';
-import session from 'express-session';
-import fetch from 'node-fetch';
-import { AgentMemory } from './shared-memory-system.js';
-
-// Import theme utilities
-const { getThemeStyles } = require('./shared-ui-components.js');
-
-const app = express();
-const PORT = 9881;
-// Initialize agent memory system
-const agentMemory = new AgentMemory('marketing');
-console.log('📚 Memory system initialized for marketing');
-
-
-// Authentication
-const AUTH_USERNAME = 'admin';
-const AUTH_PASSWORD = '2025';
-
-// Shopify configuration
-const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
-const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || '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';
-const API_VERSION = '2024-07';
-
-// Constant Contact configuration (placeholder)
-const CC_API_KEY = process.env.CONSTANT_CONTACT_API_KEY || '';
-const CC_ACCESS_TOKEN = process.env.CONSTANT_CONTACT_ACCESS_TOKEN || '';
-
-// Anthropic Claude
-const anthropic = new Anthropic({
- apiKey: process.env.ANTHROPIC_API_KEY || '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'
-});
-
-// Types for Shopify orders
-interface OrderLineItem {
- title: string;
- sku: string;
- quantity: number;
- price?: string;
-}
-
-interface ShopifyOrder {
- id: string;
- name: string;
- createdAt: string;
- totalPriceSet: {
- shopMoney: {
- amount: string;
- currencyCode: string;
- }
- };
- displayFinancialStatus: string;
- displayFulfillmentStatus: string;
- lineItems: {
- edges: Array<{
- node: OrderLineItem;
- }>;
- };
-}
-
-// Stats tracking
-let stats = {
- totalConversations: 0,
- blogPostsCreated: 0,
- socialPostsGenerated: 0,
- emailCampaignsDrafted: 0,
- lastBlogPost: null as { title: string; sku: string; timestamp: Date } | null,
- recentBlogs: [] as Array<{
- id: string;
- title: string;
- createdAt: string;
- publishedAt: string | null;
- tags: string;
- }>,
- recentProducts: [] as Array<{
- id: string;
- title: string;
- sku: string;
- vendor: string;
- handle: string;
- imageUrl: string;
- }>,
- // New order tracking stats
- recentOrders: [] as ShopifyOrder[],
- orderTrends: {
- totalRevenue: 0,
- orderCount: 0,
- averageOrderValue: 0,
- topProducts: [] as Array<{
- title: string;
- sku: string;
- totalQuantity: number;
- revenue: number;
- }>,
- dailyRevenue: [] as Array<{
- date: string;
- revenue: number;
- orderCount: number;
- }>
- }
-};
-
-app.use(express.json());
-app.use(cookieParser());
-app.use(express.urlencoded({ extended: true }));
-
-app.use(
- session({
- secret: 'dw-marketing-agent-2025',
- resave: false,
- saveUninitialized: true,
- rolling: true,
- cookie: {
- secure: false,
- httpOnly: true,
- maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
- },
- })
-);
-
-declare module 'express-session' {
- interface SessionData {
- authenticated: boolean;
- chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
- }
-}
-
-const requireAuth = (req: express.Request, res: express.Response, next: express.NextFunction) => {
- // TEMPORARY: Auth disabled for 4 hours
- return next();
-
- /* Original auth:
-
- if (req.session.authenticated) {
- next();
- } else {
- res.redirect('/login');
- }
- */
-};
-
-// Shopify API helper
-async function shopifyGraphQL(query: string, variables?: any) {
- const response = await fetch(`https://${SHOPIFY_STORE}/admin/api/${API_VERSION}/graphql.json`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Shopify-Access-Token': SHOPIFY_TOKEN,
- },
- body: JSON.stringify({ query, variables }),
- });
- return response.json();
-}
-
-// Fetch and analyze Shopify orders for marketing insights
-async function fetchAndAnalyzeOrders() {
- const query = `
- query {
- orders(first: 50, reverse: true, sortKey: CREATED_AT) {
- edges {
- node {
- id
- name
- createdAt
- totalPriceSet {
- shopMoney {
- amount
- currencyCode
- }
- }
- displayFinancialStatus
- displayFulfillmentStatus
- lineItems(first: 20) {
- edges {
- node {
- title
- sku
- quantity
- originalUnitPriceSet {
- shopMoney {
- amount
- }
- }
- }
- }
- }
- }
- }
- }
- }
- `;
-
- try {
- const response = await shopifyGraphQL(query);
-
- if (response.data?.orders?.edges) {
- const orders = response.data.orders.edges.map((edge: any) => edge.node);
- stats.recentOrders = orders;
-
- // Analyze order trends
- analyzeOrderTrends(orders);
-
- return orders;
- }
- return [];
- } catch (error) {
- console.error('Error fetching orders:', error);
- return [];
- }
-}
-
-// Analyze order data for marketing insights
-function analyzeOrderTrends(orders: ShopifyOrder[]) {
- // Reset trends
- stats.orderTrends = {
- totalRevenue: 0,
- orderCount: orders.length,
- averageOrderValue: 0,
- topProducts: [],
- dailyRevenue: []
- };
-
- // Product aggregation map
- const productMap = new Map<string, {
- title: string;
- sku: string;
- totalQuantity: number;
- revenue: number;
- }>();
-
- // Daily revenue map
- const dailyMap = new Map<string, {
- revenue: number;
- orderCount: number;
- }>();
-
- // Process each order
- orders.forEach(order => {
- const orderAmount = parseFloat(order.totalPriceSet.shopMoney.amount);
- stats.orderTrends.totalRevenue += orderAmount;
-
- // Extract date (YYYY-MM-DD)
- const orderDate = order.createdAt.split('T')[0];
- if (!dailyMap.has(orderDate)) {
- dailyMap.set(orderDate, { revenue: 0, orderCount: 0 });
- }
- const dailyData = dailyMap.get(orderDate)!;
- dailyData.revenue += orderAmount;
- dailyData.orderCount += 1;
-
- // Process line items
- order.lineItems.edges.forEach(edge => {
- const item = edge.node;
- const key = item.sku || item.title;
-
- if (!productMap.has(key)) {
- productMap.set(key, {
- title: item.title,
- sku: item.sku || 'N/A',
- totalQuantity: 0,
- revenue: 0
- });
- }
-
- const product = productMap.get(key)!;
- product.totalQuantity += item.quantity;
- if (item.price) {
- product.revenue += parseFloat(item.price) * item.quantity;
- }
- });
- });
-
- // Calculate average order value
- stats.orderTrends.averageOrderValue = orders.length > 0
- ? stats.orderTrends.totalRevenue / orders.length
- : 0;
-
- // Convert product map to sorted array (top 10 by quantity)
- stats.orderTrends.topProducts = Array.from(productMap.values())
- .sort((a, b) => b.totalQuantity - a.totalQuantity)
- .slice(0, 10);
-
- // Convert daily map to sorted array (last 7 days)
- stats.orderTrends.dailyRevenue = Array.from(dailyMap.entries())
- .map(([date, data]) => ({
- date,
- revenue: data.revenue,
- orderCount: data.orderCount
- }))
- .sort((a, b) => b.date.localeCompare(a.date))
- .slice(0, 7);
-}
-
-// Load orders on startup
-fetchAndAnalyzeOrders().then(() => {
- console.log(`📊 Loaded ${stats.recentOrders.length} orders for analysis`);
-});
-
-// Refresh orders every 30 minutes
-setInterval(() => {
- fetchAndAnalyzeOrders();
-}, 30 * 60 * 1000);
-
-// Login page
-app.get('/login', (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Login - Marketing Agent</title>
- ${getThemeStyles('modern-minimalist')}
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- }
- .login-container {
- background: white;
- padding: 40px;
- border-radius: 15px;
- box-shadow: 0 15px 35px rgba(0,0,0,0.2);
- max-width: 400px;
- width: 100%;
- }
- h1 { color: #f5576c; text-align: center; margin-bottom: 30px; }
- input {
- width: 100%;
- padding: 12px;
- margin: 10px 0;
- border: 2px solid #e0e0e0;
- border-radius: 8px;
- }
- button {
- width: 100%;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 15px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 10px;
- }
- .error { color: red; text-align: center; margin-bottom: 15px; }
- </style>
-</head>
-<body>
- <div class="login-container">
- <h1>📢 Marketing Agent</h1>
- <p style="text-align: center; color: #666; margin-bottom: 20px;">Content Creation & Campaigns</p>
- ${req.query.error ? '<div class="error">Invalid credentials</div>' : ''}
- <form method="POST">
- <input type="text" name="username" placeholder="Username" required>
- <input type="password" name="password" placeholder="Password" required>
- <button type="submit">Login</button>
- </form>
- </div>
-</body>
-</html>
- `);
-});
-
-app.post('/login', (req, res) => {
- const { username, password } = req.body;
- if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
- req.session.authenticated = true;
- req.session.chatHistory = [];
- res.redirect('/');
- } else {
- res.redirect('/login?error=1');
- }
-});
-
-app.get('/logout', (req, res) => {
- req.session.destroy(() => res.redirect('/login'));
-});
-
-// Main dashboard
-app.get('/', requireAuth, async (req, res) => {
- res.send(`
-<!DOCTYPE html>
-<html>
-<head>
- <title>Marketing Agent - DW-Agents</title>
- ${getThemeStyles('modern-minimalist')}
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- padding: 20px;
- min-height: 100vh;
- }
- .container {
- max-width: 1600px;
- margin: 0 auto;
- display: grid;
- grid-template-columns: 400px 1fr;
- gap: 20px;
- }
-
- /* Mobile-responsive sidebar */
- .sidebar {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .sidebar-toggle {
- display: none;
- position: fixed;
- top: 20px;
- left: 20px;
- z-index: 1001;
- background: white;
- border: none;
- border-radius: 50%;
- width: 50px;
- height: 50px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
- cursor: pointer;
- font-size: 1.5em;
- color: #f5576c;
- transition: transform 0.3s;
- }
- .sidebar-toggle:hover {
- transform: scale(1.1);
- }
- .sidebar-toggle:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
-
- .header {
- background: white;
- padding: 30px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .header h1 { color: #d8194e; font-size: 2em; margin-bottom: 10px; }
- .header .subtitle { color: #333; font-size: 1.1em; }
- .stats-card {
- background: white;
- padding: 20px;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- }
- .stats-card h3 { color: #555; font-size: 0.85em; text-transform: uppercase; margin-bottom: 8px; }
- .stats-card .value { font-size: 2em; font-weight: bold; color: #d8194e; }
- .main-content {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .tab-container {
- background: white;
- border-radius: 15px;
- box-shadow: 0 5px 20px rgba(0,0,0,0.1);
- overflow: hidden;
- }
-
- /* Scrollable tabs for mobile */
- .tabs {
- display: flex;
- background: #f5f5f5;
- border-bottom: 2px solid #e0e0e0;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- scrollbar-width: thin;
- }
- .tabs::-webkit-scrollbar {
- height: 4px;
- }
- .tabs::-webkit-scrollbar-track {
- background: #f1f1f1;
- }
- .tabs::-webkit-scrollbar-thumb {
- background: #f5576c;
- border-radius: 4px;
- }
- .tab {
- flex: 1;
- min-width: 140px;
- padding: 15px;
- text-align: center;
- cursor: pointer;
- font-weight: 600;
- color: #333;
- transition: all 0.3s;
- white-space: nowrap;
- }
- .tab:hover {
- background: #f0f0f0;
- }
- .tab:focus {
- outline: 3px solid #f5576c;
- outline-offset: -3px;
- z-index: 1;
- }
- .tab.active {
- background: white;
- color: #d8194e;
- border-bottom: 3px solid #d8194e;
- }
- .tab-content {
- padding: 30px;
- display: none;
- }
- .tab-content.active {
- display: block;
- }
-
- /* Loading spinner */
- .spinner {
- display: inline-block;
- width: 20px;
- height: 20px;
- border: 3px solid rgba(255,255,255,0.3);
- border-radius: 50%;
- border-top-color: white;
- animation: spin 0.8s linear infinite;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- .loading-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 9999;
- }
- .loading-overlay.active {
- display: flex;
- }
- .loading-content {
- background: white;
- padding: 30px 40px;
- border-radius: 15px;
- text-align: center;
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
- }
- .loading-spinner {
- width: 50px;
- height: 50px;
- border: 4px solid #f0f0f0;
- border-top-color: #f5576c;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin: 0 auto 15px;
- }
-
- /* Toast notifications */
- .toast {
- position: fixed;
- top: 20px;
- right: 20px;
- background: white;
- padding: 16px 24px;
- border-radius: 10px;
- box-shadow: 0 8px 24px rgba(0,0,0,0.2);
- display: flex;
- align-items: center;
- gap: 12px;
- z-index: 10000;
- transform: translateX(400px);
- transition: transform 0.3s ease-out;
- max-width: 350px;
- }
- .toast.show {
- transform: translateX(0);
- }
- .toast.error {
- border-left: 4px solid #dc3545;
- }
- .toast.success {
- border-left: 4px solid #28a745;
- }
- .toast.info {
- border-left: 4px solid #17a2b8;
- }
- .toast-icon {
- font-size: 1.5em;
- flex-shrink: 0;
- }
- .toast-message {
- flex: 1;
- color: #333;
- line-height: 1.4;
- }
- .toast-close {
- background: none;
- border: none;
- color: #888;
- cursor: pointer;
- font-size: 1.2em;
- padding: 0;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- flex-shrink: 0;
- }
- .toast-close:hover {
- background: #f0f0f0;
- color: #333;
- }
- .toast-close:focus {
- outline: 2px solid #f5576c;
- }
-
- .chat-container {
- height: 500px;
- display: flex;
- flex-direction: column;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- background: #f5f5f5;
- border-radius: 10px;
- margin-bottom: 20px;
- }
- .message {
- margin-bottom: 15px;
- display: flex;
- gap: 10px;
- }
- .message.user {
- flex-direction: row-reverse;
- }
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2em;
- flex-shrink: 0;
- }
- .message.user .message-avatar {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- }
- .message.assistant .message-avatar {
- background: #e0e0e0;
- }
- .message-content {
- max-width: 70%;
- padding: 12px 16px;
- border-radius: 12px;
- line-height: 1.5;
- }
- .message.user .message-content {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border-bottom-right-radius: 4px;
- }
- .message.assistant .message-content {
- background: white;
- color: #1a1a1a;
- border-bottom-left-radius: 4px;
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
- }
- .chat-input-container {
- display: flex;
- gap: 10px;
- }
- #chatInput {
- flex: 1;
- padding: 12px 16px;
- border: 2px solid #e0e0e0;
- border-radius: 25px;
- font-size: 1em;
- outline: none;
- color: #1a1a1a;
- }
- #chatInput:focus {
- border-color: #f5576c;
- box-shadow: 0 0 0 3px rgba(245, 87, 108, 0.1);
- }
- .send-btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 12px 30px;
- border-radius: 25px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.2s;
- }
- .send-btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
- }
- .send-btn:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
- .send-btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .send-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
- .product-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 15px;
- max-height: 400px;
- overflow-y: auto;
- }
- .product-card {
- border: 2px solid #e0e0e0;
- border-radius: 10px;
- padding: 15px;
- cursor: pointer;
- transition: all 0.3s;
- }
- .product-card:hover {
- border-color: #f5576c;
- transform: translateY(-2px);
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
- }
- .product-card:focus {
- outline: 3px solid #f5576c;
- outline-offset: 2px;
- }
- .product-card.selected {
- border-color: #f5576c;
- background: #fff5f7;
- }
- .product-image {
- width: 100%;
- height: 150px;
- object-fit: cover;
- border-radius: 8px;
- margin-bottom: 10px;
- }
- .product-title {
- font-weight: 600;
- font-size: 0.9em;
- margin-bottom: 5px;
- color: #1a1a1a;
- }
- .product-sku {
- font-size: 0.8em;
- color: #555;
- }
- .blog-list {
- max-height: 400px;
- overflow-y: auto;
- }
- .blog-item {
- padding: 15px;
- border-bottom: 1px solid #e0e0e0;
- cursor: pointer;
- transition: background 0.2s;
- }
- .blog-item:hover {
- background: #f5f5f5;
- }
- .blog-item:focus {
- outline: 2px solid #f5576c;
- outline-offset: -2px;
- background: #f5f5f5;
- }
- .blog-title {
- font-weight: 600;
- color: #1a1a1a;
- margin-bottom: 5px;
- }
- .blog-meta {
- font-size: 0.85em;
- color: #555;
- }
- .btn {
- background: linear-gradient(135deg, #36454f 0%, #708090 100%);
- color: white;
- border: none;
- padding: 12px 24px;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- margin-top: 15px;
- transition: all 0.2s;
- }
- .btn:hover:not(:disabled) {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
- }
- .btn:focus {
- outline: 3px solid #d8194e;
- outline-offset: 2px;
- }
- .btn:active:not(:disabled) {
- transform: translateY(0);
- }
- .btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .suggestions {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-bottom: 15px;
- }
- .suggestion-chip {
- background: #f0f0f0;
- padding: 8px 15px;
- border-radius: 20px;
- font-size: 0.85em;
- cursor: pointer;
- transition: all 0.2s;
- color: #333;
- }
- .suggestion-chip:hover {
- background: #f5576c;
- color: white;
- }
- .suggestion-chip:focus {
- outline: 2px solid #f5576c;
- outline-offset: 2px;
- }
-
- /* Mobile responsive */
- @media (max-width: 1024px) {
- .container {
- grid-template-columns: 1fr;
- }
- .sidebar {
- position: fixed;
- left: 0;
- top: 0;
- bottom: 0;
- width: 300px;
- background: white;
- z-index: 1000;
- padding: 20px;
- overflow-y: auto;
- transform: translateX(-100%);
- transition: transform 0.3s ease-out;
- box-shadow: 2px 0 8px rgba(0,0,0,0.1);
- }
- .sidebar.open {
- transform: translateX(0);
- }
- .sidebar-toggle {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .sidebar-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- z-index: 999;
- }
- .sidebar-overlay.active {
- display: block;
- }
- .main-content {
- margin-top: 70px;
- }
- .tab {
- font-size: 0.9em;
- padding: 12px 10px;
- }
- .message-content {
- max-width: 85%;
- }
- }
-
- @media (max-width: 640px) {
- body {
- padding: 10px;
- }
- .tab-content {
- padding: 20px 15px;
- }
- .chat-container {
- height: 400px;
- }
- .product-grid {
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
- gap: 10px;
- }
- .toast {
- right: 10px;
- left: 10px;
- max-width: none;
- }
- }
- </style>
-</head>
-<body>
- <!-- Mobile sidebar toggle -->
- <button class="sidebar-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">☰</button>
- <div class="sidebar-overlay" onclick="toggleSidebar()"></div>
-
- <!-- Loading overlay -->
- <div class="loading-overlay" id="loadingOverlay">
- <div class="loading-content">
- <div class="loading-spinner"></div>
- <div style="color: #333; font-weight: 600;">Processing...</div>
- </div>
- </div>
-
- <div class="container">
- <div class="sidebar" id="sidebar">
- <div class="header">
- <h1>📢 Marketing Agent</h1>
- <div class="subtitle">Content Creation & Campaigns</div>
- <div style="margin-top: 15px;">
- <a href="/logout" style="color: #f5576c; text-decoration: none;">Logout</a>
- </div>
- </div>
-
- <div class="stats-card">
- <h3>Blog Posts Created</h3>
- <div class="value" id="blogPostsCreated">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Social Posts</h3>
- <div class="value" id="socialPostsGenerated">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Email Campaigns</h3>
- <div class="value" id="emailCampaignsDrafted">0</div>
- </div>
-
- <div class="stats-card">
- <h3>Conversations</h3>
- <div class="value" id="totalConversations">0</div>
- </div>
- </div>
-
- <div class="main-content">
- <div class="tab-container">
- <div class="tabs">
- <div class="tab active" onclick="switchTab('insights')" tabindex="0" role="tab" aria-selected="true" onkeypress="if(event.key==='Enter') switchTab('insights')">📊 Order Insights</div>
- <div class="tab" onclick="switchTab('chat')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('chat')">💬 Chat Assistant</div>
- <div class="tab" onclick="switchTab('blogs')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('blogs')">📝 Blog Posts</div>
- <div class="tab" onclick="switchTab('create')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('create')">✨ Create from SKU</div>
- <div class="tab" onclick="switchTab('social')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('social')">📱 Social Media</div>
- </div>
-
- <!-- Order Insights Tab -->
- <div class="tab-content active" id="insights-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Order Insights & Marketing Metrics</h2>
-
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px;">
- <div style="background: #f5f5f5; padding: 20px; border-radius: 10px; border-left: 4px solid #f5576c;">
- <h3 style="color: #666; font-size: 0.9em; margin-bottom: 8px;">Total Revenue (30 days)</h3>
- <div style="font-size: 1.8em; font-weight: bold; color: #333;">$<span id="totalRevenue">0</span></div>
- </div>
- <div style="background: #f5f5f5; padding: 20px; border-radius: 10px; border-left: 4px solid #36454f;">
- <h3 style="color: #666; font-size: 0.9em; margin-bottom: 8px;">Total Orders</h3>
- <div style="font-size: 1.8em; font-weight: bold; color: #333;"><span id="orderCount">0</span></div>
- </div>
- <div style="background: #f5f5f5; padding: 20px; border-radius: 10px; border-left: 4px solid #708090;">
- <h3 style="color: #666; font-size: 0.9em; margin-bottom: 8px;">Average Order Value</h3>
- <div style="font-size: 1.8em; font-weight: bold; color: #333;">$<span id="avgOrderValue">0</span></div>
- </div>
- </div>
-
- <h3 style="color: #f5576c; margin-bottom: 15px;">🔥 Top Selling Products</h3>
- <div id="topProducts" style="background: #f5f5f5; border-radius: 10px; padding: 20px; margin-bottom: 30px;">
- <p style="color: #888;">Loading top products...</p>
- </div>
-
- <h3 style="color: #f5576c; margin-bottom: 15px;">📈 Daily Revenue Trend</h3>
- <div id="dailyRevenue" style="background: #f5f5f5; border-radius: 10px; padding: 20px; margin-bottom: 30px;">
- <p style="color: #888;">Loading revenue trends...</p>
- </div>
-
- <h3 style="color: #f5576c; margin-bottom: 15px;">🛍️ Recent Orders</h3>
- <button class="btn" onclick="loadOrderInsights()">🔄 Refresh Order Data</button>
- <div id="recentOrders" style="margin-top: 20px; max-height: 400px; overflow-y: auto;">
- <p style="color: #888; padding: 20px;">Click "Refresh Order Data" to load recent orders...</p>
- </div>
- </div>
-
- <!-- Chat Tab -->
- <div class="tab-content" id="chat-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Chat with Marketing Agent</h2>
- <div class="suggestions">
- <div class="suggestion-chip" onclick="sendSuggestion('Create a professional blog post for product grasscloth in Architectural Digest style with images')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Create a professional blog post for product grasscloth in Architectural Digest style with images')">📰 AD-Style Blog</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Make blog for vendor York with photos and details')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Make blog for vendor York with photos and details')">✨ Vendor Blog</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Generate Instagram post')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Generate Instagram post')">📸 Instagram</div>
- <div class="suggestion-chip" onclick="sendSuggestion('Show me recent blog posts')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Show me recent blog posts')">📝 Recent Blogs</div>
- </div>
- <div class="chat-container">
- <div class="chat-messages" id="chatMessages">
- <div class="message assistant">
- <div class="message-avatar">🤖</div>
- <div class="message-content">
- Hi! I'm your Marketing Agent, specialized in Architectural Digest-style editorial content.<br><br>
- <strong>I can create:</strong><br>
- • Professional blog posts with product images and links<br>
- • Editorial-style content in AD magazine format<br>
- • Social media posts (Instagram, Facebook, Pinterest)<br>
- • Product-focused articles with full details<br>
- <br>
- <strong>Try asking:</strong><br>
- "Make blog for product [name] by vendor [vendor] in professional style"<br>
- "Create AD-style blog for SKU [sku] with photos and details"<br>
- <br>
- What would you like to create today?
- </div>
- </div>
- </div>
- <div class="chat-input-container">
- <input
- type="text"
- id="chatInput"
- placeholder="Ask me to create content, view blogs, or generate posts..."
- onkeypress="if(event.key === 'Enter') sendMessage()"
- >
- <button class="send-btn" onclick="sendMessage()" id="sendBtn">Send</button>
- </div>
- </div>
- </div>
-
- <!-- Blog Posts Tab -->
- <div class="tab-content" id="blogs-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Recent Blog Posts from Shopify</h2>
- <button class="btn" onclick="loadBlogs()">🔄 Refresh Blog List</button>
- <div class="blog-list" id="blogList">
- <p style="color: #888; padding: 20px;">Click "Refresh Blog List" to load recent posts from Shopify...</p>
- </div>
- </div>
-
- <!-- Create from SKU Tab -->
- <div class="tab-content" id="create-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Create Blog Post from Product</h2>
- <button class="btn" onclick="loadProducts()">🔄 Load Recent Products</button>
- <p style="color: #666; margin: 15px 0;">Select a product below to generate a blog post:</p>
- <div class="product-grid" id="productGrid">
- <p style="color: #888; padding: 20px; grid-column: 1/-1;">Click "Load Recent Products" to see available SKUs...</p>
- </div>
- <button class="btn" id="generateBlogBtn" style="display:none;" onclick="generateBlog()">✨ Generate Blog Post</button>
- </div>
-
- <!-- Social Media Tab -->
- <div class="tab-content" id="social-content">
- <h2 style="color: #f5576c; margin-bottom: 15px;">Social Media Content Generator</h2>
- <p style="color: #666; margin-bottom: 20px;">Generate social media posts for Instagram, Facebook, and more!</p>
- <button class="btn" onclick="generateSocial('instagram')">📸 Generate Instagram Post</button>
- <button class="btn" onclick="generateSocial('facebook')">👍 Generate Facebook Post</button>
- <button class="btn" onclick="generateSocial('pinterest')">📌 Generate Pinterest Description</button>
- <div id="socialOutput" style="margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 10px; display: none;">
- <h3 style="color: #f5576c; margin-bottom: 10px;">Generated Content:</h3>
- <pre id="socialContent" style="white-space: pre-wrap; font-family: inherit; color: #333;"></pre>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <script>
- let selectedProduct = null;
-
- // Toast notification system
- function showToast(message, type = 'info') {
- const existingToast = document.querySelector('.toast');
- if (existingToast) {
- existingToast.remove();
- }
-
- const toast = document.createElement('div');
- toast.className = \`toast \${type}\`;
-
- const icons = {
- success: '✓',
- error: '✕',
- info: 'ℹ'
- };
-
- toast.innerHTML = \`
- <div class="toast-icon">\${icons[type] || icons.info}</div>
- <div class="toast-message">\${message}</div>
- <button class="toast-close" onclick="this.parentElement.remove()" aria-label="Close">×</button>
- \`;
-
- document.body.appendChild(toast);
-
- setTimeout(() => toast.classList.add('show'), 10);
-
- setTimeout(() => {
- toast.classList.remove('show');
- setTimeout(() => toast.remove(), 300);
- }, 5000);
- }
-
- // Loading overlay functions
- function showLoading() {
- document.getElementById('loadingOverlay').classList.add('active');
- }
-
- function hideLoading() {
- document.getElementById('loadingOverlay').classList.remove('active');
- }
-
- // Mobile sidebar toggle
- function toggleSidebar() {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.querySelector('.sidebar-overlay');
- sidebar.classList.toggle('open');
- overlay.classList.toggle('active');
- }
-
- function switchTab(tab) {
- // Update tab states
- document.querySelectorAll('.tab').forEach(t => {
- t.classList.remove('active');
- t.setAttribute('aria-selected', 'false');
- });
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
-
- // Activate selected tab
- event.target.classList.add('active');
- event.target.setAttribute('aria-selected', 'true');
- document.getElementById(tab + '-content').classList.add('active');
-
- // Close sidebar on mobile after tab switch
- if (window.innerWidth <= 1024) {
- const sidebar = document.getElementById('sidebar');
- const overlay = document.querySelector('.sidebar-overlay');
- if (sidebar.classList.contains('open')) {
- sidebar.classList.remove('open');
- overlay.classList.remove('active');
- }
- }
- }
-
- function updateStats() {
- fetch('/api/stats')
- .then(r => r.json())
- .then(data => {
- document.getElementById('blogPostsCreated').textContent = data.blogPostsCreated;
- document.getElementById('socialPostsGenerated').textContent = data.socialPostsGenerated;
- document.getElementById('emailCampaignsDrafted').textContent = data.emailCampaignsDrafted;
- document.getElementById('totalConversations').textContent = data.totalConversations;
- });
- }
-
- function addMessage(role, content) {
- const messagesDiv = document.getElementById('chatMessages');
- const messageDiv = document.createElement('div');
- messageDiv.className = \`message \${role}\`;
-
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = content.replace(/\\n/g, '<br>');
-
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
- messagesDiv.appendChild(messageDiv);
-
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
- }
-
- async function sendMessage() {
- const input = document.getElementById('chatInput');
- const sendBtn = document.getElementById('sendBtn');
- const message = input.value.trim();
-
- if (!message) return;
-
- addMessage('user', message);
- input.value = '';
- sendBtn.disabled = true;
- sendBtn.innerHTML = '<span class="spinner"></span>';
- showLoading();
-
- try {
- const response = await fetch('/api/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ message })
- });
-
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
-
- const data = await response.json();
- addMessage('assistant', data.response);
- updateStats();
-
- } catch (error) {
- console.error('Chat error:', error);
- addMessage('assistant', 'Sorry, I encountered an error. Please try again.');
- showToast('Failed to send message. Please try again.', 'error');
- } finally {
- hideLoading();
- sendBtn.disabled = false;
- sendBtn.textContent = 'Send';
- }
- }
-
- function sendSuggestion(text) {
- document.getElementById('chatInput').value = text;
- sendMessage();
- }
-
- async function loadBlogs() {
- const blogList = document.getElementById('blogList');
- blogList.innerHTML = '<p style="padding: 20px; color: #888;">Loading blog posts...</p>';
- showLoading();
-
- try {
- const response = await fetch('/api/shopify/blogs');
-
- if (!response.ok) {
- throw new Error('Failed to fetch blogs');
- }
-
- const data = await response.json();
-
- if (data.blogs && data.blogs.length > 0) {
- const shopifyDomain = 'designer-laboratory-sandbox.myshopify.com';
- blogList.innerHTML = data.blogs.map(blog => {
- const blogUrl = \`https://\${shopifyDomain}/blogs/news/\${blog.handle}\`;
- return \`
- <div class="blog-item" tabindex="0">
- <div class="blog-title">
- <a href="\${blogUrl}" target="_blank" style="color: #f5576c; text-decoration: none;">
- \${blog.title}
- </a>
- </div>
- <div class="blog-meta">
- Created: \${new Date(blog.createdAt).toLocaleDateString()} |
- \${blog.publishedAt ? 'Published' : 'Draft'}
- \${blog.tags ? ' | Tags: ' + blog.tags : ''}
- <br>
- <a href="\${blogUrl}" target="_blank" style="color: #666; font-size: 12px; text-decoration: none;">
- 🔗 View on Shopify →
- </a>
- </div>
- </div>
- \`;
- }).join('');
- showToast(\`Loaded \${data.blogs.length} blog posts\`, 'success');
- } else {
- blogList.innerHTML = '<p style="padding: 20px; color: #888;">No blog posts found.</p>';
- showToast('No blog posts found', 'info');
- }
- } catch (error) {
- console.error('Error loading blogs:', error);
- blogList.innerHTML = '<p style="padding: 20px; color: #dc3545;">Error loading blogs. Please try again.</p>';
- showToast('Failed to load blog posts', 'error');
- } finally {
- hideLoading();
- }
- }
-
- async function loadProducts() {
- const productGrid = document.getElementById('productGrid');
- productGrid.innerHTML = '<p style="padding: 20px; color: #888; grid-column: 1/-1;">Loading products...</p>';
- showLoading();
-
- try {
- const response = await fetch('/api/shopify/products');
-
- if (!response.ok) {
- throw new Error('Failed to fetch products');
- }
-
- const data = await response.json();
-
- if (data.products && data.products.length > 0) {
- productGrid.innerHTML = data.products.map(product => \`
- <div class="product-card" tabindex="0" onclick="selectProduct('\${product.id}', '\${product.sku}', '\${product.title}', '\${product.imageUrl}')" onkeypress="if(event.key==='Enter') selectProduct('\${product.id}', '\${product.sku}', '\${product.title}', '\${product.imageUrl}')">
- <img src="\${product.imageUrl || 'https://via.placeholder.com/200'}" class="product-image" alt="\${product.title}">
- <div class="product-title">\${product.title}</div>
- <div class="product-sku">SKU: \${product.sku}</div>
- </div>
- \`).join('');
- showToast(\`Loaded \${data.products.length} products\`, 'success');
- } else {
- productGrid.innerHTML = '<p style="padding: 20px; color: #888; grid-column: 1/-1;">No recent products found.</p>';
- showToast('No products found', 'info');
- }
- } catch (error) {
- console.error('Error loading products:', error);
- productGrid.innerHTML = '<p style="padding: 20px; color: #dc3545; grid-column: 1/-1;">Error loading products. Please try again.</p>';
- showToast('Failed to load products', 'error');
- } finally {
- hideLoading();
- }
- }
-
- function selectProduct(id, sku, title, imageUrl) {
- document.querySelectorAll('.product-card').forEach(card => card.classList.remove('selected'));
- event.currentTarget.classList.add('selected');
- selectedProduct = { id, sku, title, imageUrl };
- document.getElementById('generateBlogBtn').style.display = 'block';
- }
-
- async function generateBlog() {
- if (!selectedProduct) return;
-
- const btn = document.getElementById('generateBlogBtn');
- btn.disabled = true;
- btn.innerHTML = '<span class="spinner"></span> Generating...';
- showLoading();
-
- try {
- const response = await fetch('/api/generate/blog', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(selectedProduct)
- });
-
- if (!response.ok) {
- throw new Error('Failed to generate blog');
- }
-
- const data = await response.json();
-
- if (data.success) {
- showToast(\`Blog post created: "\${data.blog.title}"\`, 'success');
- updateStats();
- setTimeout(() => {
- switchTab('blogs');
- loadBlogs();
- }, 500);
- } else {
- throw new Error(data.error || 'Unknown error');
- }
-
- } catch (error) {
- console.error('Error generating blog:', error);
- showToast('Failed to generate blog post. Please try again.', 'error');
- } finally {
- hideLoading();
- btn.disabled = false;
- btn.textContent = '✨ Generate Blog Post';
- }
- }
-
- async function generateSocial(platform) {
- const output = document.getElementById('socialOutput');
- const content = document.getElementById('socialContent');
-
- output.style.display = 'block';
- content.textContent = 'Generating ' + platform + ' content...';
- showLoading();
-
- try {
- const response = await fetch('/api/generate/social', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ platform })
- });
-
- if (!response.ok) {
- throw new Error('Failed to generate social content');
- }
-
- const data = await response.json();
-
- if (data.success) {
- content.textContent = data.content;
- updateStats();
- showToast(\`Generated \${platform} post successfully\`, 'success');
- } else {
- throw new Error('Generation failed');
- }
-
- } catch (error) {
- console.error('Error generating social content:', error);
- content.textContent = 'Error generating content. Please try again.';
- showToast('Failed to generate social content', 'error');
- } finally {
- hideLoading();
- }
- }
-
- // Function to load order insights
- async function loadOrderInsights() {
- showLoading();
- try {
- const response = await fetch('/api/shopify/orders');
- if (!response.ok) {
- throw new Error('Failed to fetch orders');
- }
-
- const data = await response.json();
-
- if (data.success && data.orderTrends) {
- // Update summary metrics
- document.getElementById('totalRevenue').textContent = data.orderTrends.totalRevenue.toFixed(2);
- document.getElementById('orderCount').textContent = data.orderTrends.orderCount;
- document.getElementById('avgOrderValue').textContent = data.orderTrends.averageOrderValue.toFixed(2);
-
- // Display top products
- const topProductsDiv = document.getElementById('topProducts');
- if (data.orderTrends.topProducts && data.orderTrends.topProducts.length > 0) {
- topProductsDiv.innerHTML = '<table style="width: 100%; border-collapse: collapse;">' +
- '<thead><tr style="border-bottom: 2px solid #ddd;">' +
- '<th style="text-align: left; padding: 8px; color: #666;">Product</th>' +
- '<th style="text-align: center; padding: 8px; color: #666;">SKU</th>' +
- '<th style="text-align: center; padding: 8px; color: #666;">Qty Sold</th>' +
- '</tr></thead><tbody>' +
- data.orderTrends.topProducts.map((p, i) =>
- `<tr style="border-bottom: 1px solid #eee;">
- <td style="padding: 8px; color: #333;"><strong>${i+1}.</strong> ${p.title}</td>
- <td style="text-align: center; padding: 8px; color: #666; font-size: 0.9em;">${p.sku}</td>
- <td style="text-align: center; padding: 8px; color: #f5576c; font-weight: bold;">${p.totalQuantity}</td>
- </tr>`
- ).join('') +
- '</tbody></table>';
- } else {
- topProductsDiv.innerHTML = '<p style="color: #888;">No product data available</p>';
- }
-
- // Display daily revenue trend
- const dailyRevenueDiv = document.getElementById('dailyRevenue');
- if (data.orderTrends.dailyRevenue && data.orderTrends.dailyRevenue.length > 0) {
- dailyRevenueDiv.innerHTML = '<table style="width: 100%; border-collapse: collapse;">' +
- '<thead><tr style="border-bottom: 2px solid #ddd;">' +
- '<th style="text-align: left; padding: 8px; color: #666;">Date</th>' +
- '<th style="text-align: center; padding: 8px; color: #666;">Orders</th>' +
- '<th style="text-align: right; padding: 8px; color: #666;">Revenue</th>' +
- '</tr></thead><tbody>' +
- data.orderTrends.dailyRevenue.map(d => {
- const date = new Date(d.date);
- const formattedDate = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
- return `<tr style="border-bottom: 1px solid #eee;">
- <td style="padding: 8px; color: #333;">${formattedDate}</td>
- <td style="text-align: center; padding: 8px; color: #666;">${d.orderCount}</td>
- <td style="text-align: right; padding: 8px; color: #28a745; font-weight: bold;">$${d.revenue.toFixed(2)}</td>
- </tr>`;
- }).join('') +
- '</tbody></table>';
- } else {
- dailyRevenueDiv.innerHTML = '<p style="color: #888;">No revenue data available</p>';
- }
-
- // Display recent orders
- const ordersDiv = document.getElementById('recentOrders');
- if (data.recentOrders && data.recentOrders.length > 0) {
- ordersDiv.innerHTML = data.recentOrders.map(order => {
- const orderDate = new Date(order.createdAt);
- const items = order.lineItems.edges.map(e => e.node);
- return `
- <div style="background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin-bottom: 10px;">
- <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 10px;">
- <div>
- <strong style="color: #f5576c;">${order.name}</strong>
- <span style="color: #666; margin-left: 10px; font-size: 0.9em;">
- ${orderDate.toLocaleDateString()} ${orderDate.toLocaleTimeString()}
- </span>
- </div>
- <div style="text-align: right; margin-left: auto;">
- <span style="font-size: 1.2em; font-weight: bold; color: #28a745;">
- $${parseFloat(order.totalPriceSet.shopMoney.amount).toFixed(2)}
- </span>
- </div>
- </div>
- <div style="color: #666; font-size: 0.9em;">
- ${items.slice(0, 3).map(item =>
- `• ${item.title} ${item.sku ? `(${item.sku})` : ''} × ${item.quantity}`
- ).join('<br>')}
- ${items.length > 3 ? `<br>• ... and ${items.length - 3} more items` : ''}
- </div>
- <div style="margin-top: 8px;">
- <span style="display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8em; background: #e8f4f8; color: #0066cc;">
- ${order.displayFinancialStatus}
- </span>
- ${order.displayFulfillmentStatus ?
- `<span style="display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8em; background: #f0f8e8; color: #5a8a00; margin-left: 5px;">
- ${order.displayFulfillmentStatus}
- </span>` : ''
- }
- </div>
- </div>
- `;
- }).join('');
- showToast(`Loaded ${data.recentOrders.length} recent orders`, 'success');
- } else {
- ordersDiv.innerHTML = '<p style="color: #888; padding: 20px;">No orders found</p>';
- }
-
- } else {
- throw new Error('Invalid response format');
- }
-
- } catch (error) {
- console.error('Error loading order insights:', error);
- showToast('Failed to load order insights', 'error');
- document.getElementById('topProducts').innerHTML = '<p style="color: #dc3545;">Error loading data</p>';
- document.getElementById('dailyRevenue').innerHTML = '<p style="color: #dc3545;">Error loading data</p>';
- document.getElementById('recentOrders').innerHTML = '<p style="color: #dc3545;">Error loading orders</p>';
- } finally {
- hideLoading();
- }
- }
-
- // Load order insights on page load
- setTimeout(() => {
- loadOrderInsights();
- }, 1000);
-
- // Update stats every 10 seconds
- setInterval(updateStats, 10000);
- updateStats();
- </script>
-</body>
-</html>
- `);
-});
-
-// API endpoints
-app.get('/api/stats', requireAuth, (req, res) => {
- res.json(stats);
-});
-
-app.post('/api/chat', requireAuth, async (req, res) => {
- try {
- const { message } = req.body;
-
- if (!message) {
- return res.status(400).json({ error: 'Message is required' });
- }
-
- // Memory system commands
- if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
- const parts = message.split(':');
- const type = parts[0].toLowerCase();
- const content = parts.slice(1).join(':').trim();
- const memType = type === 'remember' ? 'note' : type as any;
- agentMemory.add(content, memType);
- return res.json({
- response: `✅ Memory saved: "${content}"`,
- success: true,
- memoryAdded: true
- });
- }
-
- if (message.toLowerCase().includes('show') && message.toLowerCase().includes('memor')) {
- const summary = agentMemory.getContextSummary();
- return res.json({ response: summary, success: true });
- }
-
- if (message.toLowerCase().includes('search memor')) {
- const query = message.split(/for|about/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- return res.json({
- response: results.length > 0
- ? 'Found memories:\n' + results.map(m => `- ${m.content}`).join('\n')
- : 'No matching memories found.',
- success: true
- });
- }
-
- if (message.toLowerCase().includes('forget') || message.toLowerCase().includes('delete memor')) {
- const query = message.split(/forget|delete/i)[1]?.trim() || '';
- const results = agentMemory.search(query);
- if (results.length > 0) {
- agentMemory.delete(results[0].id);
- return res.json({ response: `✅ Forgot: "${results[0].content}"`, success: true });
- }
- return res.json({ response: 'Nothing found to forget.', success: true });
- }
-
- stats.totalConversations++;
-
- if (!req.session.chatHistory) {
- req.session.chatHistory = [];
- }
-
- req.session.chatHistory.push({ role: 'user', content: message });
-
- // Parse the message for product-specific requests
- let productContext = '';
- const productMatch = message.match(/product\s+([^,\s]+)|vendor\s+([^,\s]+)|sku\s+([^,\s]+)/i);
-
- if (productMatch) {
- // Search for the product in Shopify
- const searchTerm = productMatch[1] || productMatch[2] || productMatch[3];
- try {
- const query = `query {
- products(first: 5, query: "${searchTerm}") {
- edges {
- node {
- id
- title
- handle
- vendor
- description
- variants(first: 1) {
- edges {
- node {
- sku
- price
- }
- }
- }
- featuredImage {
- url
- altText
- }
- images(first: 5) {
- edges {
- node {
- url
- altText
- }
- }
- }
- }
- }
- }
- }`;
- const productData = await shopifyGraphQL(query);
-
- if (productData.data?.products?.edges?.length > 0) {
- const products = productData.data.products.edges.map((edge: any) => ({
- title: edge.node.title,
- vendor: edge.node.vendor,
- sku: edge.node.variants.edges[0]?.node.sku || 'N/A',
- price: edge.node.variants.edges[0]?.node.price || 'N/A',
- description: edge.node.description,
- mainImage: edge.node.featuredImage?.url || '',
- images: edge.node.images.edges.map((img: any) => img.node.url),
- shopifyUrl: `https://${SHOPIFY_STORE}/products/${edge.node.handle}`
- }));
-
- productContext = `\n\nFOUND PRODUCTS:\n${products.map(p =>
- `- ${p.title} by ${p.vendor} (SKU: ${p.sku})
- Price: $${p.price}
- Description: ${p.description?.substring(0, 200)}...
- Main Image: ${p.mainImage}
- All Images: ${p.images.join(', ')}
- Product URL: ${p.shopifyUrl}`
- ).join('\n\n')}`;
- }
- } catch (err) {
- console.error('Error searching products:', err);
- }
- }
-
- const systemPrompt = `You are the Marketing Agent for Designer Wallcoverings, a specialist in creating Architectural Digest-style editorial content.
-
-Your responsibilities:
-1. Create compelling, magazine-quality blog posts from product SKUs
-2. Generate social media content (Instagram, Facebook, Pinterest)
-3. Draft email marketing campaigns
-4. Write engaging product descriptions in professional editorial style
-5. Provide marketing strategy advice
-
-BLOG POST CREATION SKILLS:
-When asked to create a blog post for a product, you should:
-- Write in the sophisticated, editorial style of Architectural Digest
-- Include proper HTML structure with headings, paragraphs, and semantic markup
-- Reference product images with full URLs in <img> tags and proper alt text
-- Link to the product page using the provided Shopify URL with descriptive anchor text
-- Describe the product's design, texture, color, and styling possibilities
-- Reference interior design trends and room applications
-- Use designer terminology and sophisticated language
-- Include multiple product images throughout the post
-- Add proper meta description and SEO keywords
-- Structure: Opening hook → Product details → Design applications → Styling tips → Call-to-action
-
-PRODUCT DATA AVAILABLE:${productContext}
-
-You have access to:
-- Shopify store with products and blog posts
-- Product details including images, descriptions, SKUs, prices, vendors
-- Direct product URLs for linking
-
-Current statistics:
-- Blog posts created: ${stats.blogPostsCreated}
-- Social posts generated: ${stats.socialPostsGenerated}
-- Email campaigns: ${stats.emailCampaignsDrafted}
-
-Recent blog posts:
-${stats.recentBlogs.length > 0 ? stats.recentBlogs.slice(-3).map(blog =>
- `- "${blog.title}" (created ${new Date(blog.createdAt).toLocaleDateString()})`
-).join('\n') : '- No recent blogs'}
-
-${agentMemory.getContextSummary()}
-
-When creating content:
-1. Use sophisticated, editorial writing style
-2. Include actual image URLs from the product data
-3. Create proper HTML with href links to product pages
-4. Reference specific product details (SKU, vendor, price)
-5. Write compelling, benefit-focused copy
-6. Use SEO-optimized headlines and descriptions
-7. Reference stored memories and preferences when relevant
-
-Be professional, sophisticated, and magazine-editorial in your approach.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 4096, // Increased for longer blog posts
- system: systemPrompt,
- messages: req.session.chatHistory
- });
-
- const assistantMessage = response.content[0].type === 'text'
- ? response.content[0].text
- : 'I apologize, but I encountered an error.';
-
- req.session.chatHistory.push({ role: 'assistant', content: assistantMessage });
-
- if (req.session.chatHistory.length > 20) {
- req.session.chatHistory = req.session.chatHistory.slice(-20);
- }
-
- res.json({ response: assistantMessage });
-
- } catch (error) {
- console.error('Chat error:', error);
- res.status(500).json({
- error: 'Failed to process message',
- response: 'I apologize, but I encountered an error. Please try again.'
- });
- }
-});
-
-// Shopify blog posts
-app.get('/api/shopify/blogs', requireAuth, async (req, res) => {
- try {
- const query = `
- query {
- articles(first: 10, sortKey: CREATED_AT, reverse: true) {
- edges {
- node {
- id
- title
- createdAt
- publishedAt
- tags
- handle
- }
- }
- }
- }
- `;
-
- const data = await shopifyGraphQL(query);
-
- if (data.data && data.data.articles) {
- const blogs = data.data.articles.edges.map((edge: any) => ({
- id: edge.node.id,
- title: edge.node.title,
- createdAt: edge.node.createdAt,
- publishedAt: edge.node.publishedAt,
- tags: edge.node.tags.join(', '),
- handle: edge.node.handle
- }));
-
- stats.recentBlogs = blogs;
- res.json({ blogs });
- } else {
- res.json({ blogs: [] });
- }
-
- } catch (error) {
- console.error('Error fetching blogs:', error);
- res.status(500).json({ error: 'Failed to fetch blogs' });
- }
-});
-
-// Shopify products
-app.get('/api/shopify/products', requireAuth, async (req, res) => {
- try {
- const query = `
- query {
- products(first: 20, sortKey: CREATED_AT, reverse: true) {
- edges {
- node {
- id
- title
- handle
- vendor
- variants(first: 1) {
- edges {
- node {
- sku
- }
- }
- }
- featuredImage {
- url
- }
- }
- }
- }
- }
- `;
-
- const data = await shopifyGraphQL(query);
-
- if (data.data && data.data.products) {
- const products = data.data.products.edges.map((edge: any) => ({
- id: edge.node.id,
- title: edge.node.title,
- handle: edge.node.handle,
- vendor: edge.node.vendor,
- sku: edge.node.variants.edges[0]?.node.sku || 'N/A',
- imageUrl: edge.node.featuredImage?.url || ''
- }));
-
- stats.recentProducts = products;
- res.json({ products });
- } else {
- res.json({ products: [] });
- }
-
- } catch (error) {
- console.error('Error fetching products:', error);
- res.status(500).json({ error: 'Failed to fetch products' });
- }
-});
-
-// Generate blog post from product
-app.post('/api/generate/blog', requireAuth, async (req, res) => {
- try {
- const { id, sku, title, imageUrl } = req.body;
-
- // Use Claude to generate blog content
- const prompt = `Create a compelling blog post for this wallcovering product:
-
-Product Title: ${title}
-SKU: ${sku}
-
-Write an engaging blog post (300-500 words) that includes:
-1. An attention-grabbing headline
-2. Introduction highlighting the product's unique features
-3. Design trends and styling suggestions
-4. Room application ideas
-5. Call-to-action
-
-Make it SEO-friendly and engaging for interior designers and homeowners.`;
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 2048,
- messages: [{ role: 'user', content: prompt }]
- });
-
- const blogContent = response.content[0].type === 'text' ? response.content[0].text : '';
-
- // Extract title (first line) and body
- const lines = blogContent.split('\n');
- const blogTitle = lines[0].replace(/^#+ /, '').trim();
- const blogBody = lines.slice(1).join('\n').trim();
-
- stats.blogPostsCreated++;
- stats.lastBlogPost = {
- title: blogTitle,
- sku,
- timestamp: new Date()
- };
-
- // In production, would create blog post via Shopify API
- res.json({
- success: true,
- blog: {
- title: blogTitle,
- content: blogBody,
- sku,
- productId: id
- }
- });
-
- } catch (error) {
- console.error('Error generating blog:', error);
- res.status(500).json({ error: 'Failed to generate blog post' });
- }
-});
-
-// Generate social media content
-app.post('/api/generate/social', requireAuth, async (req, res) => {
- try {
- const { platform } = req.body;
-
- const prompts: Record<string, string> = {
- instagram: 'Write an engaging Instagram caption (150 chars) for a new designer wallcovering collection. Include emojis and 5 relevant hashtags.',
- facebook: 'Write a Facebook post (200 chars) announcing a new wallcovering line. Make it informative and include a call-to-action.',
- pinterest: 'Write a Pinterest pin description (300 chars) for wallcovering design inspiration. Focus on style and room transformation.'
- };
-
- const response = await anthropic.messages.create({
- model: 'claude-sonnet-4-20250514',
- max_tokens: 512,
- messages: [{ role: 'user', content: prompts[platform] || prompts.instagram }]
- });
-
- const content = response.content[0].type === 'text' ? response.content[0].text : '';
-
- stats.socialPostsGenerated++;
-
- res.json({
- success: true,
- platform,
- content
- });
-
- } catch (error) {
- console.error('Error generating social content:', error);
- res.status(500).json({ error: 'Failed to generate social content' });
- }
-});
-
-// Shopify orders endpoint for marketing insights
-app.get('/api/shopify/orders', requireAuth, async (req, res) => {
- try {
- // Fetch and analyze orders
- const orders = await fetchAndAnalyzeOrders();
-
- res.json({
- success: true,
- recentOrders: orders,
- orderTrends: stats.orderTrends,
- summary: {
- totalOrders: orders.length,
- lastUpdated: new Date().toISOString()
- }
- });
-
- } catch (error) {
- console.error('Error fetching orders:', error);
- res.status(500).json({
- error: 'Failed to fetch order data',
- success: false
- });
- }
-});
-
-app.listen(PORT, '0.0.0.0', () => {
- console.log('');
- console.log('📢 Marketing Agent');
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
- console.log('🌍 External: http://45.61.58.125:' + PORT);
- console.log('🏠 Local: http://localhost:' + PORT);
- console.log('');
- console.log('💬 Chat Mode: Active');
- console.log('📝 Shopify Blog Integration: Ready');
- console.log('✨ AI Content Generation: Active');
- console.log('📱 Social Media Tools: Ready');
- console.log('📧 Email Campaigns: Ready');
- console.log('');
- console.log('✅ Marketing Agent ready for content creation...');
-});
← 6c0fe229 auto-data-snapshot: 2026-09-17T15:48:34 (3 data files) — sho
·
back to Designer Wallcoverings
·
harden: escape img src in dw-boost-overrides cardHtml (was r 9d1c6b1f →