← back to Designer Wallcoverings
DW-Agents/dw-agents/marketing-agent.ts
2456 lines
/**
* 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
* 6. Google Tasks integration - read tasks from info@designerwallcoverings.com
*
* Port: 9881
*/
import { requireGlobalAuth } from './shared-global-auth';
import Anthropic from '@anthropic-ai/sdk';
import express from 'express';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import fetch from 'node-fetch';
import { AgentMemory } from './shared-memory-system.js';
import { google } from 'googleapis';
import * as fs from 'fs';
// Import theme utilities
import { getThemeStyles } from './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');
// Google Tasks API setup
const GOOGLE_TASKS_TOKEN_PATH = '/root/Projects/Designer-Wallcoverings/DW-Agents/dw-agents/google-tasks-token.json';
const GOOGLE_CREDS_PATH = '/root/Projects/Designer-Wallcoverings/DW-Programming/ImportNewSkufromURL/dw-gmail-credentials.json';
let googleTasksClient: any = null;
let googleTasksAvailable = false;
function initGoogleTasks() {
try {
if (!fs.existsSync(GOOGLE_TASKS_TOKEN_PATH)) {
console.log('⚠️ Google Tasks token not found. Run setup-google-tasks-auth.ts first.');
return;
}
if (!fs.existsSync(GOOGLE_CREDS_PATH)) {
console.log('⚠️ Google credentials not found at', GOOGLE_CREDS_PATH);
return;
}
const creds = JSON.parse(fs.readFileSync(GOOGLE_CREDS_PATH, 'utf8'));
const tokens = JSON.parse(fs.readFileSync(GOOGLE_TASKS_TOKEN_PATH, 'utf8'));
const clientId = creds.installed?.client_id || creds.client_id;
const clientSecret = creds.installed?.client_secret || creds.client_secret;
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret);
oauth2Client.setCredentials(tokens);
// Auto-refresh tokens
oauth2Client.on('tokens', (newTokens: any) => {
const merged = { ...tokens, ...newTokens };
fs.writeFileSync(GOOGLE_TASKS_TOKEN_PATH, JSON.stringify(merged, null, 2));
console.log('🔄 Google Tasks token refreshed');
});
googleTasksClient = google.tasks({ version: 'v1', auth: oauth2Client });
googleTasksAvailable = true;
console.log('✅ Google Tasks API initialized');
} catch (err: any) {
console.error('❌ Google Tasks init error:', err.message);
}
}
initGoogleTasks();
// Cache for Google Tasks data
let cachedTaskLists: any[] = [];
let cachedTasks: Map<string, any[]> = new Map();
let lastTasksFetch = 0;
const TASKS_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
async function fetchGoogleTasks(forceRefresh = false) {
if (!googleTasksAvailable || !googleTasksClient) return { lists: [], tasks: new Map() };
const now = Date.now();
if (!forceRefresh && now - lastTasksFetch < TASKS_CACHE_TTL && cachedTaskLists.length > 0) {
return { lists: cachedTaskLists, tasks: cachedTasks };
}
try {
const listRes = await googleTasksClient.tasklists.list({ maxResults: 20 });
cachedTaskLists = listRes.data.items || [];
cachedTasks = new Map();
for (const list of cachedTaskLists) {
try {
const tasksRes = await googleTasksClient.tasks.list({
tasklist: list.id,
maxResults: 100,
showCompleted: true,
showHidden: true,
});
cachedTasks.set(list.id, tasksRes.data.items || []);
} catch (err: any) {
console.error(`Error fetching tasks for list ${list.title}:`, err.message);
cachedTasks.set(list.id, []);
}
}
lastTasksFetch = now;
return { lists: cachedTaskLists, tasks: cachedTasks };
} catch (err: any) {
console.error('Error fetching Google Tasks:', err.message);
return { lists: cachedTaskLists, tasks: cachedTasks };
}
}
// Authentication
// Shopify configuration
const SHOPIFY_STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
const SHOPIFY_TOKEN = process.env.SHOPIFY_ACCESS_TOKEN || '';
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 || "${ANTHROPIC_API_KEY}"
});
// 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(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(requireGlobalAuth);
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 }>;
}
}
// 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
// Main dashboard
app.get('/', requireGlobalAuth, 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 class="tab" onclick="switchTab('gtasks')" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('gtasks')">📋 Google Tasks <span id="gtasksBadge" style="background:#f5576c;color:white;border-radius:10px;padding:1px 7px;font-size:0.75em;margin-left:4px;display:none;">0</span></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>
<!-- Google Tasks Tab -->
<div class="tab-content" id="gtasks-content">
<h2 style="color: #f5576c; margin-bottom: 15px;">Google Tasks - Marketing Inbox</h2>
<p style="color: #666; margin-bottom: 15px;">Tasks from info@designerwallcoverings.com Google Tasks. Click a task to generate content.</p>
<div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap;">
<button class="btn" onclick="loadGoogleTasks(true)" style="margin:0;">🔄 Refresh Tasks</button>
<select id="gtasksFilter" onchange="filterGoogleTasks()" style="padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 0.95em; color: #333; background: white;">
<option value="needsAction">Pending Tasks</option>
<option value="completed">Completed Tasks</option>
<option value="all">All Tasks</option>
</select>
</div>
<div id="gtasksStatus" style="display: none; background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 15px; margin-bottom: 15px; color: #856404;">
⚠️ Google Tasks not configured. Run <code>npx tsx setup-google-tasks-auth.ts</code> in the dw-agents directory to connect.
</div>
<div id="gtasksList" style="max-height: 600px; overflow-y: auto;">
<p style="color: #888; padding: 20px;">Loading Google Tasks...</p>
</div>
<!-- Content generation modal -->
<div id="gtasksModal" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:9999; justify-content:center; align-items:center;">
<div style="background:white; border-radius:15px; padding:30px; max-width:700px; width:90%; max-height:80vh; overflow-y:auto; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h3 style="color:#f5576c; margin:0;" id="gtasksModalTitle">Generate Content</h3>
<button onclick="closeGtasksModal()" style="background:none; border:none; font-size:1.5em; cursor:pointer; color:#888;">×</button>
</div>
<div id="gtasksModalTask" style="background:#f5f5f5; padding:15px; border-radius:8px; margin-bottom:20px;">
<div style="font-weight:600; color:#333;" id="gtasksModalTaskTitle"></div>
<div style="color:#666; margin-top:5px; font-size:0.9em;" id="gtasksModalTaskNotes"></div>
</div>
<div style="margin-bottom:15px;">
<label style="color:#555; font-weight:600; display:block; margin-bottom:8px;">Content Type:</label>
<div style="display:flex; gap:10px; flex-wrap:wrap;">
<button class="btn" onclick="processGoogleTask('blog')" id="gtasksBlogBtn" style="margin:0;">📝 Blog Post</button>
<button class="btn" onclick="processGoogleTask('social')" id="gtasksSocialBtn" style="margin:0;">📱 Social Media</button>
<button class="btn" onclick="processGoogleTask('auto')" id="gtasksAutoBtn" style="margin:0;">✨ Auto-detect</button>
</div>
</div>
<div style="margin-bottom:15px;">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; color:#555;">
<input type="checkbox" id="gtasksMarkComplete" style="width:18px; height:18px;">
Mark task as completed after generating content
</label>
</div>
<div id="gtasksModalOutput" style="display:none; background:#f5f5f5; padding:20px; border-radius:8px; margin-top:15px;">
<h4 style="color:#f5576c; margin-bottom:10px;">Generated Content:</h4>
<pre id="gtasksModalContent" style="white-space:pre-wrap; font-family:inherit; color:#333; max-height:300px; overflow-y:auto;"></pre>
</div>
</div>
</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" data-id="\${product.id}" data-sku="\${product.sku}" data-title="\${product.title}" data-image="\${product.imageUrl}" tabindex="0">
<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(card) {
document.querySelectorAll('.product-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedProduct = {
id: card.dataset.id,
sku: card.dataset.sku,
title: card.dataset.title,
imageUrl: card.dataset.image
};
document.getElementById('generateBlogBtn').style.display = 'block';
}
// Add event delegation for product cards
document.getElementById('productGrid').addEventListener('click', (e) => {
const card = e.target.closest('.product-card');
if (card) selectProduct(card);
});
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);
// ========================
// Google Tasks Functions
// ========================
let gtasksData = null;
let selectedGTask = null;
async function loadGoogleTasks(forceRefresh) {
const listDiv = document.getElementById('gtasksList');
const statusDiv = document.getElementById('gtasksStatus');
listDiv.innerHTML = '<p style="padding:20px; color:#888;">Loading Google Tasks...</p>';
try {
const url = forceRefresh ? '/api/google-tasks?refresh=true' : '/api/google-tasks';
const response = await fetch(url);
const data = await response.json();
if (!data.available) {
statusDiv.style.display = 'block';
listDiv.innerHTML = '<p style="padding:20px; color:#888;">Connect Google Tasks to see your marketing inbox here.</p>';
return;
}
statusDiv.style.display = 'none';
gtasksData = data;
renderGoogleTasks();
if (forceRefresh) showToast('Google Tasks refreshed', 'success');
} catch (error) {
console.error('Error loading Google Tasks:', error);
listDiv.innerHTML = '<p style="padding:20px; color:#dc3545;">Error loading Google Tasks.</p>';
showToast('Failed to load Google Tasks', 'error');
}
}
function renderGoogleTasks() {
if (!gtasksData) return;
const listDiv = document.getElementById('gtasksList');
const filter = document.getElementById('gtasksFilter').value;
let html = '';
let totalPending = 0;
for (const list of gtasksData.lists) {
let tasks = list.tasks || [];
if (filter === 'needsAction') tasks = tasks.filter(t => t.status === 'needsAction');
else if (filter === 'completed') tasks = tasks.filter(t => t.status === 'completed');
const pendingCount = (list.tasks || []).filter(t => t.status === 'needsAction').length;
totalPending += pendingCount;
html += '<div style="margin-bottom:20px;">';
html += '<h3 style="color:#36454f; margin-bottom:10px; padding-bottom:5px; border-bottom:2px solid #e0e0e0;">' +
list.title + ' <span style="color:#888; font-size:0.8em;">(' + pendingCount + ' pending)</span></h3>';
if (tasks.length === 0) {
html += '<p style="color:#888; padding:10px;">No tasks match this filter.</p>';
} else {
for (const task of tasks) {
const isPending = task.status === 'needsAction';
const dueStr = task.due ? new Date(task.due).toLocaleDateString() : '';
const statusIcon = isPending ? '⬜' : '✅';
const statusColor = isPending ? '#f5576c' : '#28a745';
html += '<div style="padding:12px 15px; border:1px solid #e0e0e0; border-left:4px solid ' + statusColor + '; border-radius:8px; margin-bottom:8px; cursor:pointer; transition:all 0.2s; background:white;" ' +
'onmouseover="this.style.boxShadow=\\'0 2px 8px rgba(0,0,0,0.1)\\'; this.style.transform=\\'translateX(4px)\\'" ' +
'onmouseout="this.style.boxShadow=\\'none\\'; this.style.transform=\\'none\\'" ' +
'onclick="openGtasksModal(\\'' + list.id + '\\', \\'' + task.id + '\\')">';
html += '<div style="display:flex; justify-content:space-between; align-items:flex-start;">';
html += '<div style="flex:1;">';
html += '<div style="font-weight:600; color:#333;">' + statusIcon + ' ' + (task.title || 'Untitled') + '</div>';
if (task.notes) {
html += '<div style="color:#666; font-size:0.85em; margin-top:4px; max-height:40px; overflow:hidden;">' + task.notes.substring(0, 150) + (task.notes.length > 150 ? '...' : '') + '</div>';
}
html += '</div>';
html += '<div style="text-align:right; flex-shrink:0; margin-left:10px;">';
if (dueStr) html += '<div style="color:#f5576c; font-size:0.8em;">Due: ' + dueStr + '</div>';
if (isPending) html += '<div style="color:#f5576c; font-size:0.75em; margin-top:4px;">Click to generate content →</div>';
html += '</div></div></div>';
}
}
html += '</div>';
}
if (gtasksData.lists.length === 0) {
html = '<p style="padding:20px; color:#888;">No task lists found in Google Tasks.</p>';
}
listDiv.innerHTML = html;
// Update badge
const badge = document.getElementById('gtasksBadge');
if (totalPending > 0) {
badge.textContent = totalPending;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
function filterGoogleTasks() {
renderGoogleTasks();
}
function openGtasksModal(listId, taskId) {
if (!gtasksData) return;
const list = gtasksData.lists.find(l => l.id === listId);
if (!list) return;
const task = list.tasks.find(t => t.id === taskId);
if (!task) return;
selectedGTask = { listId, taskId, task };
document.getElementById('gtasksModalTitle').textContent = 'Generate Content from Task';
document.getElementById('gtasksModalTaskTitle').textContent = task.title || 'Untitled';
document.getElementById('gtasksModalTaskNotes').textContent = task.notes || 'No notes';
document.getElementById('gtasksModalOutput').style.display = 'none';
document.getElementById('gtasksMarkComplete').checked = false;
const modal = document.getElementById('gtasksModal');
modal.style.display = 'flex';
// Disable buttons if completed
const isCompleted = task.status === 'completed';
document.getElementById('gtasksBlogBtn').disabled = false;
document.getElementById('gtasksSocialBtn').disabled = false;
document.getElementById('gtasksAutoBtn').disabled = false;
}
function closeGtasksModal() {
document.getElementById('gtasksModal').style.display = 'none';
selectedGTask = null;
}
async function processGoogleTask(contentType) {
if (!selectedGTask) return;
const btns = ['gtasksBlogBtn', 'gtasksSocialBtn', 'gtasksAutoBtn'];
btns.forEach(id => { document.getElementById(id).disabled = true; });
showLoading();
try {
const response = await fetch('/api/google-tasks/process', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
taskId: selectedGTask.taskId,
listId: selectedGTask.listId,
contentType,
markComplete: document.getElementById('gtasksMarkComplete').checked
})
});
const data = await response.json();
if (data.success) {
document.getElementById('gtasksModalContent').textContent = data.content;
document.getElementById('gtasksModalOutput').style.display = 'block';
showToast('Content generated from "' + data.taskTitle + '"', 'success');
updateStats();
if (data.markedComplete) {
loadGoogleTasks(true);
}
} else {
throw new Error(data.error || 'Failed to process task');
}
} catch (error) {
console.error('Error processing Google Task:', error);
showToast('Failed to generate content', 'error');
} finally {
hideLoading();
btns.forEach(id => { document.getElementById(id).disabled = false; });
}
}
// Load Google Tasks on startup
setTimeout(() => loadGoogleTasks(false), 2000);
// Refresh Google Tasks badge every 60 seconds
setInterval(async () => {
try {
const resp = await fetch('/api/google-tasks/stats');
const data = await resp.json();
const badge = document.getElementById('gtasksBadge');
if (data.available && data.pending > 0) {
badge.textContent = data.pending;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
} catch (e) {}
}, 60000);
// Update stats every 10 seconds
setInterval(updateStats, 10000);
updateStats();
</script>
</body>
</html>
`);
});
// API endpoints
app.get('/api/stats', (req, res) => {
res.json(stats);
});
app.post('/api/chat', 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-opus-4-8',
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', 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', 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', 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-opus-4-8',
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', 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-opus-4-8',
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', 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
});
}
});
// ========================
// Google Tasks API Endpoints
// ========================
// GET /api/google-tasks - List all task lists and their tasks
app.get('/api/google-tasks', async (req, res) => {
if (!googleTasksAvailable) {
return res.json({
available: false,
message: 'Google Tasks not configured. Run setup-google-tasks-auth.ts first.',
lists: []
});
}
try {
const forceRefresh = req.query.refresh === 'true';
const { lists, tasks } = await fetchGoogleTasks(forceRefresh);
const result = lists.map(list => ({
id: list.id,
title: list.title,
updated: list.updated,
tasks: (tasks.get(list.id) || []).map((t: any) => ({
id: t.id,
title: t.title,
notes: t.notes || '',
status: t.status, // needsAction or completed
due: t.due || null,
completed: t.completed || null,
updated: t.updated,
parent: t.parent || null,
position: t.position,
links: t.links || [],
}))
}));
res.json({
available: true,
lastFetch: new Date(lastTasksFetch).toISOString(),
lists: result
});
} catch (error: any) {
console.error('Error in /api/google-tasks:', error);
res.status(500).json({ error: error.message });
}
});
// GET /api/google-tasks/stats - Quick stats for badge
app.get('/api/google-tasks/stats', async (req, res) => {
if (!googleTasksAvailable) {
return res.json({ available: false, pending: 0, completed: 0, total: 0 });
}
try {
const { lists, tasks } = await fetchGoogleTasks();
let pending = 0;
let completed = 0;
for (const taskList of tasks.values()) {
for (const task of taskList) {
if (task.status === 'completed') completed++;
else pending++;
}
}
res.json({
available: true,
pending,
completed,
total: pending + completed,
listCount: lists.length
});
} catch (error: any) {
res.json({ available: false, pending: 0, completed: 0, total: 0 });
}
});
// POST /api/google-tasks/process - Process a task into marketing content
app.post('/api/google-tasks/process', async (req, res) => {
try {
const { taskId, listId, contentType } = req.body;
if (!googleTasksAvailable) {
return res.status(400).json({ error: 'Google Tasks not configured' });
}
// Fetch the specific task
const task = await googleTasksClient.tasks.get({
tasklist: listId,
task: taskId,
});
const taskData = task.data;
const taskTitle = taskData.title || 'Untitled Task';
const taskNotes = taskData.notes || '';
const type = contentType || 'blog';
let prompt = '';
if (type === 'blog') {
prompt = `You are the Marketing Agent for Designer Wallcoverings, writing in Architectural Digest editorial style.
A task from the marketing inbox says:
Title: "${taskTitle}"
Notes: "${taskNotes}"
Create a compelling blog post (400-600 words) based on this task. Include:
1. An attention-grabbing headline
2. Professional editorial content in AD magazine style
3. Interior design context and trend references
4. Room application suggestions
5. Call-to-action to visit designerwallcoverings.com
Write in sophisticated, magazine-quality prose.`;
} else if (type === 'social') {
prompt = `You are the Marketing Agent for Designer Wallcoverings.
A task from the marketing inbox says:
Title: "${taskTitle}"
Notes: "${taskNotes}"
Generate social media content for this task:
1. Instagram caption (150 chars max, with emojis and 5 hashtags)
2. Facebook post (200 chars, informative with CTA)
3. Pinterest description (300 chars, style and transformation focused)
Format each platform separately.`;
} else {
prompt = `You are the Marketing Agent for Designer Wallcoverings.
A task from the marketing inbox says:
Title: "${taskTitle}"
Notes: "${taskNotes}"
Create marketing content for this task. Determine the best format (blog, social, email) and produce professional content.`;
}
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }]
});
const content = response.content[0].type === 'text' ? response.content[0].text : '';
// Optionally mark task as completed
if (req.body.markComplete) {
await googleTasksClient.tasks.update({
tasklist: listId,
task: taskId,
requestBody: {
...taskData,
status: 'completed',
}
});
}
if (type === 'blog') stats.blogPostsCreated++;
if (type === 'social') stats.socialPostsGenerated++;
res.json({
success: true,
taskTitle,
contentType: type,
content,
markedComplete: req.body.markComplete || false,
});
} catch (error: any) {
console.error('Error processing Google Task:', error);
res.status(500).json({ error: error.message });
}
});
// POST /api/google-tasks/complete - Mark a task as completed
app.post('/api/google-tasks/complete', async (req, res) => {
try {
const { taskId, listId } = req.body;
if (!googleTasksAvailable) {
return res.status(400).json({ error: 'Google Tasks not configured' });
}
const task = await googleTasksClient.tasks.get({
tasklist: listId,
task: taskId,
});
await googleTasksClient.tasks.update({
tasklist: listId,
task: taskId,
requestBody: {
...task.data,
status: 'completed',
}
});
// Clear cache to reflect change
lastTasksFetch = 0;
res.json({ success: true, message: 'Task marked as completed' });
} catch (error: any) {
console.error('Error completing Google Task:', error);
res.status(500).json({ error: error.message });
}
});
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('📋 Google Tasks: ' + (googleTasksAvailable ? 'Connected' : 'Not configured'));
console.log('');
console.log('✅ Marketing Agent ready for content creation...');
});