← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-marketing/marketing-agent.ts
2595 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. Collections integration for curated marketing campaigns
*
* Port: 9881
*/
import Anthropic from '@anthropic-ai/sdk';
import express from 'express';
import type { Request, Response } from 'express';
import { requireGlobalAuth } from '../shared-global-auth';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import fetch from 'node-fetch';
import { AgentMemory } from './shared-memory-system';
import { exec } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Fix __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import { chatMiddleware } from '../shared-chat-integration';
const app = express();
// Global Authentication
const PORT = 9881;
// Global Authentication - MUST come before any other middleware
app.use(cookieParser());
// Add inter-agent chat widget (after auth)
app.use(chatMiddleware({
agentId: 'agent-marketing',
agentName: 'Marketing',
port: 9881,
category: 'agent'
}));
// Initialize agent memory system
const agentMemory = new AgentMemory('marketing');
console.log('📚 Memory system initialized for marketing');
// 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}",
});
// 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;
}>
};
app.use(express.json());
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 }>;
}
}
// 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();
}
// Main dashboard
app.get('/', requireGlobalAuth, async (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Marketing Agent - DW-Agents</title>
<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, #f093fb 0%, #f5576c 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: #f8f9fa;
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: #f8f9fa;
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, #f093fb 0%, #f5576c 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, #f093fb 0%, #f5576c 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, #f093fb 0%, #f5576c 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: #f8f9fa;
}
.blog-item:focus {
outline: 2px solid #f5576c;
outline-offset: -2px;
background: #f8f9fa;
}
.blog-title {
font-weight: 600;
color: #1a1a1a;
margin-bottom: 5px;
}
.blog-meta {
font-size: 0.85em;
color: #555;
}
.btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 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;
}
}
/* Hooks Panel */
.hooks-panel {
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
margin-top: 20px;
}
.hooks-panel h3 {
color: #555;
font-size: 0.95em;
text-transform: uppercase;
margin-bottom: 15px;
font-weight: 700;
}
.hook-btn {
width: 100%;
padding: 12px 16px;
margin-bottom: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
font-size: 0.9em;
transition: all 0.3s;
text-align: left;
display: flex;
align-items: center;
gap: 8px;
}
.hook-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.hook-btn:active {
transform: translateY(0);
}
.hook-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.hook-btn.fix {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.hook-btn.monitor {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
/* Communication Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
overflow: auto;
}
.modal.show {
display: block;
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 15px;
width: 90%;
max-width: 600px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 20px 25px;
border-radius: 15px 15px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 1.3em;
}
.modal-close {
background: none;
border: none;
color: white;
font-size: 1.8em;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
}
.modal-close:hover {
background: rgba(255,255,255,0.2);
}
.modal-body {
padding: 25px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #333;
font-weight: 600;
margin-bottom: 8px;
font-size: 0.95em;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 0.95em;
font-family: inherit;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: #f5576c;
}
.form-group textarea {
min-height: 120px;
resize: vertical;
}
.modal-footer {
padding: 20px 25px;
background: #f8f9fa;
border-radius: 0 0 15px 15px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.modal-btn {
padding: 10px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
font-size: 0.95em;
}
.modal-btn-primary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.modal-btn-primary:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
}
.modal-btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.modal-btn-secondary {
background: white;
color: #666;
border: 2px solid #e0e0e0;
}
.modal-btn-secondary:hover {
background: #f0f0f0;
}
@media (max-width: 768px) {
.modal-content {
width: 95%;
margin: 10% auto;
}
}
/* Collections Tab Styles */
.collections-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
margin-top: 20px;
}
.collection-card {
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 14px;
overflow: hidden;
transition: all 0.3s;
}
.collection-card:hover {
border-color: #f5576c;
box-shadow: 0 8px 24px rgba(245,87,108,0.15);
transform: translateY(-2px);
}
.collection-card-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 16px 20px;
}
.collection-card-header h3 {
margin: 0 0 4px 0;
font-size: 1.15em;
}
.collection-card-header .collection-count {
font-size: 0.85em;
opacity: 0.9;
}
.collection-card-body {
padding: 18px 20px;
}
.collection-card-body p {
color: #555;
font-size: 0.92em;
line-height: 1.5;
margin: 0 0 14px 0;
}
.collection-thumbnails {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.collection-thumbnails img {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #eee;
}
.collection-info-box {
background: #f8f9fa;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 10px;
font-size: 0.88em;
color: #444;
line-height: 1.5;
}
.collection-info-box strong {
color: #d8194e;
}
.collection-social-btns {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.collection-social-btns button {
flex: 1;
min-width: 110px;
padding: 10px 14px;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.85em;
cursor: pointer;
transition: all 0.2s;
color: white;
}
.collection-social-btns button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
}
.collection-social-btns button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-instagram {
background: linear-gradient(135deg, #833ab4, #fd1d1d, #fcb045);
}
.btn-facebook {
background: #1877f2;
}
.btn-pinterest {
background: #e60023;
}
.btn-email {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.social-output-panel {
margin-top: 16px;
background: #f8f9fa;
border: 2px solid #e0e0e0;
border-radius: 10px;
overflow: hidden;
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.social-output-header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.social-output-header h4 {
margin: 0;
font-size: 0.95em;
}
.social-output-body {
padding: 16px;
}
.social-output-body pre {
white-space: pre-wrap;
font-family: inherit;
color: #333;
font-size: 0.92em;
line-height: 1.6;
margin: 0;
}
.social-output-body .hashtags {
margin-top: 10px;
color: #1877f2;
font-weight: 600;
font-size: 0.88em;
}
.collections-loading {
text-align: center;
padding: 40px;
color: #888;
}
.collections-loading .loading-spinner {
margin: 0 auto 15px;
}
.collections-empty {
text-align: center;
padding: 40px;
color: #888;
}
.collections-empty h3 {
color: #555;
margin-bottom: 10px;
}
.copy-btn {
background: none;
border: 1px solid rgba(255,255,255,0.5);
color: white;
padding: 4px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 0.85em;
transition: all 0.2s;
}
.copy-btn:hover {
background: rgba(255,255,255,0.2);
border-color: white;
}
</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>
<!-- System Hooks Panel -->
<div class="hooks-panel">
<h3>System Tools</h3>
<button class="hook-btn" onclick="runHook('service-health-check', 'check')">
<span>🏥</span> Health Check
</button>
<button class="hook-btn fix" onclick="runHook('service-health-check', 'fix')">
<span>🔧</span> Auto-Fix
</button>
<button class="hook-btn monitor" onclick="runHook('agent-health-monitor')">
<span>👥</span> Monitor Agents
</button>
</div>
<!-- Communication Panel -->
<div class="hooks-panel">
<h3>📧 Communication</h3>
<button class="hook-btn" onclick="openEmailModal()">
<span>📧</span> Send Email
</button>
<button class="hook-btn" onclick="openSlackModal()">
<span>💬</span> Post to Slack
</button>
<button class="hook-btn" onclick="openSMSModal()">
<span>📱</span> Send SMS
</button>
</div>
</div>
<div class="main-content">
<div class="tab-container">
<div class="tabs">
<div class="tab active" onclick="switchTab('chat', this)" tabindex="0" role="tab" aria-selected="true" onkeypress="if(event.key==='Enter') switchTab('chat', this)">💬 Chat Assistant</div>
<div class="tab" onclick="switchTab('blogs', this)" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('blogs', this)">📝 Blog Posts</div>
<div class="tab" onclick="switchTab('create', this)" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('create', this)">✨ Create from SKU</div>
<div class="tab" onclick="switchTab('social', this)" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('social', this)">📱 Social Media</div>
<div class="tab" onclick="switchTab('collections', this)" tabindex="0" role="tab" aria-selected="false" onkeypress="if(event.key==='Enter') switchTab('collections', this)">🎨 Collections</div>
</div>
<!-- Chat Tab -->
<div class="tab-content active" id="chat-content">
<h2 style="color: #f5576c; margin-bottom: 15px;">Chat with Marketing Agent</h2>
<div class="suggestions">
<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 class="suggestion-chip" onclick="sendSuggestion('Create a blog post from a new product')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Create a blog post from a new product')">✨ Create blog</div>
<div class="suggestion-chip" onclick="sendSuggestion('Generate Instagram post')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Generate Instagram post')">📸 Instagram post</div>
<div class="suggestion-chip" onclick="sendSuggestion('Draft email campaign')" tabindex="0" onkeypress="if(event.key==='Enter') sendSuggestion('Draft email campaign')">📧 Email campaign</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. I can help you:<br><br>
• Create blog posts from new SKUs<br>
• View and manage Shopify blog posts<br>
• Generate social media content<br>
• Draft email campaigns<br>
• Write compelling product descriptions<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: #f8f9fa; 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>
<!-- Collections Tab -->
<div class="tab-content" id="collections-content">
<h2 style="color: #f5576c; margin-bottom: 8px;">Curated Collections</h2>
<p style="color: #666; margin-bottom: 20px;">Browse collections from the Collection Creator and generate social media content for marketing campaigns.</p>
<button class="btn" onclick="loadCollections()">🔄 Load Collections</button>
<div id="collectionsContainer">
<div class="collections-empty">
<h3>No Collections Loaded</h3>
<p>Click "Load Collections" to fetch curated collections from the Collection Creator agent.</p>
</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, element) {
// 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
if (element) {
element.classList.add('active');
element.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) {
blogList.innerHTML = data.blogs.map(function(blog) {
return '<div class="blog-item" tabindex="0">' +
'<div class="blog-title">' + blog.title + '</div>' +
'<div class="blog-meta">' +
'Created: ' + new Date(blog.createdAt).toLocaleDateString() + ' | ' +
(blog.publishedAt ? 'Published' : 'Draft') +
(blog.tags ? ' | Tags: ' + blog.tags : '') +
'</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(function(product) {
return '<div class="product-card" tabindex="0" onclick="selectProduct(\'' + product.id + '\', \'' + product.sku + '\', \'' + product.title.replace(/'/g, "\\'") + '\', \'' + product.imageUrl + '\')" onkeypress="if(event.key===\'Enter\') selectProduct(\'' + product.id + '\', \'' + product.sku + '\', \'' + product.title.replace(/'/g, "\\'") + '\', \'' + 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();
}
}
// Hook execution function
async function runHook(hookName, action = '') {
const btn = event.target.closest('.hook-btn');
if (btn.disabled) return;
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span> Running...';
try {
const response = await fetch('/api/hooks/' + hookName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action })
});
const result = await response.json();
if (result.success) {
showToast('Hook executed successfully!', 'success');
if (result.output) {
console.log('Hook output:', result.output);
}
} else {
showToast('Hook error: ' + (result.error || 'Unknown error'), 'error');
}
} catch (err) {
console.error('Hook execution error:', err);
showToast('Error: ' + err.message, 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
// Update stats every 10 seconds
setInterval(updateStats, 10000);
updateStats();
// Communication Modal Functions
const MCP_API_URL = 'http://localhost:3001';
const MCP_AUTH = btoa('admin:password123'); // Update with actual credentials
function openEmailModal() {
document.getElementById('emailModal').classList.add('show');
}
function closeEmailModal() {
document.getElementById('emailModal').classList.remove('show');
}
function openSlackModal() {
document.getElementById('slackModal').classList.add('show');
}
function closeSlackModal() {
document.getElementById('slackModal').classList.remove('show');
}
function openSMSModal() {
document.getElementById('smsModal').classList.add('show');
}
function closeSMSModal() {
document.getElementById('smsModal').classList.remove('show');
}
async function sendEmail() {
const to = document.getElementById('emailTo').value;
const subject = document.getElementById('emailSubject').value;
const body = document.getElementById('emailBody').value;
if (!to || !subject || !body) {
showToast('Please fill in all fields', 'error');
return;
}
const btn = document.getElementById('sendEmailBtn');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const response = await fetch(MCP_API_URL + '/api/gmail/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + MCP_AUTH
},
body: JSON.stringify({ to, subject, body })
});
const data = await response.json();
if (data.success) {
showToast('Email sent successfully!', 'success');
closeEmailModal();
// Clear form
document.getElementById('emailTo').value = '';
document.getElementById('emailSubject').value = '';
document.getElementById('emailBody').value = '';
} else {
showToast('Failed to send email: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
showToast('Error sending email: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Send Email';
}
}
async function sendSlackMessage() {
const channel = document.getElementById('slackChannel').value;
const text = document.getElementById('slackMessage').value;
if (!channel || !text) {
showToast('Please fill in all fields', 'error');
return;
}
const btn = document.getElementById('sendSlackBtn');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const response = await fetch(MCP_API_URL + '/api/slack/message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + MCP_AUTH
},
body: JSON.stringify({ channel, text })
});
const data = await response.json();
if (data.success) {
showToast('Message posted to Slack!', 'success');
closeSlackModal();
// Clear form
document.getElementById('slackChannel').value = '';
document.getElementById('slackMessage').value = '';
} else {
showToast('Failed to post message: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
showToast('Error posting to Slack: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Send Message';
}
}
async function sendSMS() {
const to = document.getElementById('smsTo').value;
const body = document.getElementById('smsBody').value;
if (!to || !body) {
showToast('Please fill in all fields', 'error');
return;
}
const btn = document.getElementById('sendSMSBtn');
btn.disabled = true;
btn.textContent = 'Sending...';
try {
const response = await fetch(MCP_API_URL + '/api/twilio/sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + MCP_AUTH
},
body: JSON.stringify({ to, body })
});
const data = await response.json();
if (data.success) {
showToast('SMS sent successfully!', 'success');
closeSMSModal();
// Clear form
document.getElementById('smsTo').value = '';
document.getElementById('smsBody').value = '';
} else {
showToast('Failed to send SMS: ' + (data.error || 'Unknown error'), 'error');
}
} catch (error) {
showToast('Error sending SMS: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Send SMS';
}
}
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.classList.remove('show');
}
}
// === COLLECTIONS TAB ===
let collectionsData = [];
async function loadCollections() {
const container = document.getElementById('collectionsContainer');
container.innerHTML = '<div class="collections-loading"><div class="loading-spinner"></div><div>Loading collections from Collection Creator...</div></div>';
try {
const response = await fetch('/api/collections');
if (!response.ok) {
throw new Error('Status ' + response.status);
}
const data = await response.json();
// Support both array and object with collections key
const collections = Array.isArray(data) ? data : (data.collections || data.data || []);
collectionsData = collections;
if (collections.length === 0) {
container.innerHTML = '<div class="collections-empty"><h3>No Collections Found</h3><p>The Collection Creator agent may not have any collections yet. Create one at <a href="http://45.61.58.125:9613" target="_blank" style="color: #f5576c;">port 9613</a>.</p></div>';
showToast('No collections found', 'info');
return;
}
container.innerHTML = '<div class="collections-grid">' + collections.map(function(col, idx) {
var name = col.name || col.title || 'Untitled Collection';
var desc = col.description || col.marketing_description || 'No description available.';
var productCount = col.productCount || col.product_count || (col.products ? col.products.length : 0);
var marketing = col.marketing || col.marketing_strategy || '';
var why = col.why || col.why_this_collection || '';
// Get product thumbnails (up to 6)
var thumbs = '';
if (col.products && col.products.length > 0) {
var images = col.products.slice(0, 6);
thumbs = '<div class="collection-thumbnails">' +
images.map(function(p) {
var imgUrl = p.image || p.imageUrl || p.featured_image || '';
var pTitle = p.title || p.name || '';
return imgUrl ? '<img src="' + imgUrl + '" alt="' + pTitle.replace(/"/g, '') + '" title="' + pTitle.replace(/"/g, '') + '">' : '';
}).join('') +
'</div>';
}
var marketingBox = marketing
? '<div class="collection-info-box"><strong>Marketing Strategy:</strong> ' + marketing + '</div>'
: '';
var whyBox = why
? '<div class="collection-info-box"><strong>Why This Collection:</strong> ' + why + '</div>'
: '';
return '<div class="collection-card" id="collection-card-' + idx + '">' +
'<div class="collection-card-header">' +
'<h3>' + name + '</h3>' +
'<div class="collection-count">' + productCount + ' product' + (productCount !== 1 ? 's' : '') + '</div>' +
'</div>' +
'<div class="collection-card-body">' +
'<p>' + desc + '</p>' +
thumbs +
marketingBox +
whyBox +
'<div class="collection-social-btns">' +
'<button class="btn-instagram" onclick="genCollectionSocial(' + idx + ', \'instagram\', this)">📸 Instagram</button>' +
'<button class="btn-facebook" onclick="genCollectionSocial(' + idx + ', \'facebook\', this)">👍 Facebook</button>' +
'<button class="btn-pinterest" onclick="genCollectionSocial(' + idx + ', \'pinterest\', this)">📌 Pinterest</button>' +
'<button class="btn-email" onclick="genCollectionSocial(' + idx + ', \'email\', this)">📧 Email</button>' +
'</div>' +
'<div id="collection-social-output-' + idx + '"></div>' +
'</div>' +
'</div>';
}).join('') + '</div>';
showToast('Loaded ' + collections.length + ' collection' + (collections.length !== 1 ? 's' : ''), 'success');
} catch (error) {
console.error('Error loading collections:', error);
container.innerHTML = '<div class="collections-empty"><h3>Could Not Load Collections</h3><p style="color:#dc3545;">The Collection Creator agent at port 9613 may be offline. Error: ' + error.message + '</p><p style="margin-top:10px;"><a href="http://45.61.58.125:9613" target="_blank" style="color: #f5576c;">Open Collection Creator</a></p></div>';
showToast('Failed to load collections: ' + error.message, 'error');
}
}
async function genCollectionSocial(idx, platform, btn) {
var col = collectionsData[idx];
if (!col) return;
var name = col.name || col.title || 'Untitled';
var desc = col.description || col.marketing_description || '';
var marketing = col.marketing || col.marketing_strategy || '';
var productCount = col.productCount || col.product_count || (col.products ? col.products.length : 0);
var outputDiv = document.getElementById('collection-social-output-' + idx);
btn.disabled = true;
var originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span>';
try {
var response = await fetch('/api/generate/collection-social', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
collectionName: name,
collectionDescription: desc,
marketing: marketing,
productCount: productCount,
platform: platform
})
});
if (!response.ok) throw new Error('Status ' + response.status);
var data = await response.json();
var platformLabels = { instagram: 'Instagram', facebook: 'Facebook', pinterest: 'Pinterest', email: 'Email Campaign' };
var platformLabel = platformLabels[platform] || platform;
var captionText = data.caption || data.content || '';
var hashtagsText = data.hashtags || '';
outputDiv.innerHTML = '<div class="social-output-panel">' +
'<div class="social-output-header">' +
'<h4>' + platformLabel + ' Post for "' + name + '"</h4>' +
'<button class="copy-btn" onclick="copySocialContent(this)">Copy</button>' +
'</div>' +
'<div class="social-output-body">' +
'<pre>' + captionText + '</pre>' +
(hashtagsText ? '<div class="hashtags">' + hashtagsText + '</div>' : '') +
'</div>' +
'</div>';
showToast(platformLabel + ' post generated for "' + name + '"', 'success');
} catch (error) {
console.error('Error generating collection social post:', error);
outputDiv.innerHTML = '<div class="social-output-panel"><div class="social-output-body" style="color:#dc3545;">Failed to generate post: ' + error.message + '</div></div>';
showToast('Failed to generate social post', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
function copySocialContent(btn) {
var panel = btn.closest('.social-output-panel');
var pre = panel.querySelector('pre');
var hashtags = panel.querySelector('.hashtags');
var text = pre.textContent + (hashtags ? '\\n\\n' + hashtags.textContent : '');
navigator.clipboard.writeText(text).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
showToast('Content copied to clipboard', 'success');
}).catch(function() {
// Fallback for older browsers
var textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
});
}
</script>
<!-- Email Modal -->
<div id="emailModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>📧 Send Email</h2>
<button class="modal-close" onclick="closeEmailModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="emailTo">Recipient Email</label>
<input type="email" id="emailTo" placeholder="recipient@example.com" />
</div>
<div class="form-group">
<label for="emailSubject">Subject</label>
<input type="text" id="emailSubject" placeholder="Email subject" />
</div>
<div class="form-group">
<label for="emailBody">Message</label>
<textarea id="emailBody" placeholder="Your message here..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-secondary" onclick="closeEmailModal()">Cancel</button>
<button class="modal-btn modal-btn-primary" id="sendEmailBtn" onclick="sendEmail()">Send Email</button>
</div>
</div>
</div>
<!-- Slack Modal -->
<div id="slackModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>💬 Post to Slack</h2>
<button class="modal-close" onclick="closeSlackModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="slackChannel">Channel</label>
<input type="text" id="slackChannel" placeholder="#general or @username" />
</div>
<div class="form-group">
<label for="slackMessage">Message</label>
<textarea id="slackMessage" placeholder="Your message here..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-secondary" onclick="closeSlackModal()">Cancel</button>
<button class="modal-btn modal-btn-primary" id="sendSlackBtn" onclick="sendSlackMessage()">Send Message</button>
</div>
</div>
</div>
<!-- SMS Modal -->
<div id="smsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>📱 Send SMS</h2>
<button class="modal-close" onclick="closeSMSModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="smsTo">Phone Number</label>
<input type="tel" id="smsTo" placeholder="+1234567890" />
</div>
<div class="form-group">
<label for="smsBody">Message</label>
<textarea id="smsBody" placeholder="Your message here..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="modal-btn modal-btn-secondary" onclick="closeSMSModal()">Cancel</button>
<button class="modal-btn modal-btn-primary" id="sendSMSBtn" onclick="sendSMS()">Send SMS</button>
</div>
</div>
</div>
</body>
</html>
`);
});
// API endpoints
app.get('/api/stats', requireGlobalAuth, (req, res) => {
res.json(stats);
});
app.post('/api/chat', requireGlobalAuth, async (req, res) => {
// 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 });
}
try {
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
stats.totalConversations++;
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
req.session.chatHistory.push({ role: 'user', content: message });
const systemPrompt = `You are the Marketing Agent for Designer Wallcoverings.
Your responsibilities:
1. Create compelling blog posts from product SKUs
2. Generate social media content (Instagram, Facebook, Pinterest)
3. Draft email marketing campaigns
4. Write engaging product descriptions
5. Provide marketing strategy advice
You have access to:
- Shopify store with products and blog posts
- Recent product additions and SKUs
- Constant Contact for email campaigns
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 asked to create content:
1. Be creative and engaging
2. Use compelling headlines
3. Include SEO keywords naturally
4. Focus on benefits and style trends
5. Use designer wallcovering terminology
6. Reference stored memories and preferences when relevant
---
## OLDER-ADULT MOBILE UI DESIGN SYSTEM (Over 65 UI)
When building or designing mobile UIs for older adults (55-85+), follow these specifications:
### COLOR SYSTEM
BG_BASE: #F5F2EC (warm off-white), TEXT_PRIMARY: #1A1A1A
PRIMARY: #0B4C8C (deep navy), ACCENT: #F2B705
- Never use pure white #FFF full screen
- Maintain contrast ratio ≥ 7:1
- Avoid green/yellow, blue/purple confusion pairs
### TYPOGRAPHY (MOBILE)
FONT_FAMILY: Inter, SF Pro, Roboto, Helvetica, sans-serif
SIZES: BODY 18px, LABEL 20px, BUTTON_TEXT 22px, TITLE 28px, H1 34px
- Always bold for labels & buttons
- Never use thin or light weights
- Left-align all text, minimum line-height: 1.4
### MOBILE BUTTON SYSTEM
BUTTON_HEIGHT: 64px, BUTTON_RADIUS: 16px, BUTTON_TEXT_SIZE: 22px
BUTTON_ICON_SIZE: 28px, HIT_AREA_MIN: 56px x 56px
- PRIMARY_BUTTON: background PRIMARY, text white, height 64px
- SECONDARY_BUTTON: background #D9C8A0, text TEXT_PRIMARY
- All buttons include icon + label (no icon-only or text-only)
### TOUCH TARGETS & INTERACTION
TOUCH_TARGET_MIN: 56px, SPACING_MIN: 24px, LIST_ROW_HEIGHT: 72px
INPUT_HEIGHT: 64px, TOGGLE_HEIGHT: 48px
- Large centered/left-aligned interactive elements
- Tall list rows with large icons (28-32px)
- No tiny edge-to-edge tap zones
### LAYOUT RULES (MOBILE)
PADDING: 24px horizontal, 24-32px vertical, CARD_RADIUS: 18px
- Single-column layouts
- Large cards to separate actions
- Max 4 items per screen without scroll
- Bottom navigation with labels
### NAVIGATION
- Bottom nav bar with text labels (20px), icons 28-32px
- Back button always visible (32px, hit area 56px)
- Header height 72px, title 26-28px
### COMPONENTS
Required: Large button with icon, Card panel, Large input (64px), Toggle (32-36px track),
List item (72px height), Alert banner (high contrast)
### OUTPUT REQUIREMENTS
Generate mobile UI code using this system in React Native, Flutter, SwiftUI, Jetpack Compose,
or HTML/CSS for mobile. Automatically implement older-adult accessibility.
---
Be enthusiastic, creative, and marketing-focused in your responses.`;
const response = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
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', requireGlobalAuth, 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', requireGlobalAuth, 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', requireGlobalAuth, 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()
};
// Create blog post in Shopify as DRAFT
try {
// First, get the blog ID (we'll use the first blog, or "News" blog)
const blogsQuery = `
query {
blogs(first: 1) {
edges {
node {
id
}
}
}
}
`;
const blogsData = await shopifyGraphQL(blogsQuery);
const blogId = blogsData?.data?.blogs?.edges?.[0]?.node?.id;
if (!blogId) {
// No blog found - return generated content without creating
return res.json({
success: true,
blog: {
title: blogTitle,
content: blogBody,
sku,
productId: id,
status: 'generated_only',
message: 'No blog found in Shopify to publish to'
}
});
}
// Create article as DRAFT
const createArticleMutation = `
mutation articleCreate($article: ArticleCreateInput!, $blogId: ID!) {
articleCreate(article: $article, blogId: $blogId) {
article {
id
title
handle
publishedAt
}
userErrors {
field
message
}
}
}
`;
const variables = {
blogId: blogId,
article: {
title: blogTitle,
bodyHtml: blogBody.replace(/\n/g, '<br>'),
tags: [sku, 'AI-Generated'],
published: false // This creates it as DRAFT
}
};
const createData = await shopifyGraphQL(createArticleMutation, variables);
if (createData?.data?.articleCreate?.userErrors?.length > 0) {
console.error('Shopify article creation errors:', createData.data.articleCreate.userErrors);
return res.json({
success: true,
blog: {
title: blogTitle,
content: blogBody,
sku,
productId: id,
status: 'generated_only',
message: 'Generated but not published to Shopify: ' + createData.data.articleCreate.userErrors[0].message
}
});
}
res.json({
success: true,
blog: {
title: blogTitle,
content: blogBody,
sku,
productId: id,
shopifyArticleId: createData?.data?.articleCreate?.article?.id,
handle: createData?.data?.articleCreate?.article?.handle,
status: 'draft',
message: 'Blog post created as DRAFT in Shopify'
}
});
} catch (shopifyError) {
console.error('Error creating Shopify blog post:', shopifyError);
// Still return the generated content even if Shopify creation fails
res.json({
success: true,
blog: {
title: blogTitle,
content: blogBody,
sku,
productId: id,
status: 'generated_only',
message: 'Generated content but failed to create in Shopify'
}
});
}
} 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', requireGlobalAuth, 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' });
}
});
// Metrics endpoint for daily reporting
app.get("/api/metrics", (req: Request, res: Response) => {
res.json({
status: "online",
uptime: process.uptime(),
responseTime: 0,
tasksCompleted: 0,
errorsToday: 0,
lastActivity: new Date().toISOString(),
qnaReadiness: 100
});
});
// Hook execution endpoint
app.post('/api/hooks/:hookName', requireGlobalAuth, async (req, res) => {
const { hookName } = req.params;
const { action } = req.body;
try {
const cmd = `node /root/Projects/Designer-Wallcoverings/DW-MCP/DW-Hooks/${hookName}-hook.js ${action || ''}`.trim();
exec(cmd, { timeout: 30000 }, (error, stdout, stderr) => {
if (error) {
console.error('Hook execution error:', error);
return res.status(500).json({
success: false,
error: stderr || error.message
});
}
res.json({
success: true,
output: stdout
});
});
} catch (err) {
console.error('Hook error:', err);
res.status(500).json({
success: false,
error: err.message
});
}
});
// === COLLECTION CREATOR INTEGRATION ===
app.get('/api/collections', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const http = require('http');
const data: any = await new Promise((resolve, reject) => {
const authStr = Buffer.from(process.env.BASIC_AUTH).toString('base64');
const r = http.request({
hostname: '127.0.0.1',
port: 9613,
path: '/api/collections',
method: 'GET',
headers: { 'Authorization': `Basic ${authStr}` },
timeout: 10000
}, (resp: any) => {
let d = '';
resp.on('data', (c: string) => d += c);
resp.on('end', () => {
try { resolve(JSON.parse(d)); } catch (e) { reject(e); }
});
});
r.on('error', reject);
r.on('timeout', () => { r.destroy(); reject(new Error('Connection timed out')); });
r.end();
});
res.json(data);
} catch (err: any) {
res.status(503).json({ error: 'Collection Creator unavailable', details: err.message });
}
});
// Generate social media post from a collection
app.post('/api/generate/collection-social', requireGlobalAuth, async (req: Request, res: Response) => {
try {
const { collectionName, collectionDescription, marketing, productCount, platform } = req.body;
const platformGuide: Record<string, string> = {
instagram: 'Write an Instagram caption (max 2200 chars) with relevant hashtags. Use emojis sparingly. Include a call to action to visit designerwallcoverings.com.',
facebook: 'Write a Facebook post (200-400 words) that tells a story about this collection. Engaging, shareable, and include a link to designerwallcoverings.com.',
pinterest: 'Write a Pinterest pin description (max 500 chars) with relevant keywords for search discovery. Focus on design inspiration.',
email: 'Write a marketing email subject line and preview text (2-3 sentences) for this collection launch announcement.',
};
const guide = platformGuide[platform] || platformGuide.instagram;
// Use Anthropic Claude to generate compelling social content
try {
const prompt = `You are a luxury wallcovering marketing expert for Designer Wallcoverings.
Generate a ${platform} social media post for this curated collection:
Collection Name: ${collectionName}
Description: ${collectionDescription || 'A curated collection of premium wallcoverings.'}
Marketing Strategy: ${marketing || 'Showcase luxury and design excellence.'}
Number of Products: ${productCount || 'multiple'}
${guide}
Write the post content only. Do not include any preamble or explanation. For Instagram, put hashtags on a separate line at the end.`;
const aiResponse = await anthropic.messages.create({
model: 'claude-opus-4-8',
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }]
});
const generatedText = aiResponse.content[0].type === 'text' ? aiResponse.content[0].text : '';
// Split hashtags from caption for Instagram
let caption = generatedText;
let hashtags = '';
if (platform === 'instagram') {
const lines = generatedText.split('\n');
const hashtagLines: string[] = [];
const captionLines: string[] = [];
for (const line of lines) {
if (line.trim().startsWith('#') || (line.match(/#/g) || []).length >= 3) {
hashtagLines.push(line.trim());
} else {
captionLines.push(line);
}
}
caption = captionLines.join('\n').trim();
hashtags = hashtagLines.join(' ').trim() || `#wallcovering #interiordesign #${collectionName.replace(/[^a-zA-Z]/g, '')} #designerwallcoverings #luxuryinteriors`;
} else {
hashtags = `#wallcovering #interiordesign #${collectionName.replace(/[^a-zA-Z]/g, '')} #designerwallcoverings`;
}
stats.socialPostsGenerated++;
res.json({
platform: platform || 'instagram',
collection: collectionName,
caption,
hashtags,
guide
});
} catch (aiError: any) {
// Fallback to template if AI fails
console.error('AI generation failed, using template:', aiError.message);
const content = {
platform: platform || 'instagram',
collection: collectionName,
caption: `Introducing our ${collectionName} collection \u2014 ${collectionDescription || 'A curated selection of premium wallcoverings.'}. ${marketing || 'Designed for discerning interiors.'} Shop the full collection of ${productCount || ''} curated wallcoverings at designerwallcoverings.com.`,
hashtags: `#wallcovering #interiordesign #${collectionName.replace(/[^a-zA-Z]/g, '')} #designerwallcoverings #commercialdesign #hospitality`,
guide
};
res.json(content);
}
} catch (err: any) {
res.status(500).json({ error: err.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('🎨 Collections Integration: Ready');
console.log('');
console.log('✅ Marketing Agent ready for content creation...');
});