// PromptsView.jsx — AI Prompts with Premium Presentation Mode
const { useState, useEffect, useRef, useMemo } = React;
function syntaxHighlight(jsonObj) {
let json = typeof jsonObj !== 'string' ? JSON.stringify(jsonObj, null, 2) : jsonObj;
if (typeof jsonObj === 'string') {
try {
const parsed = JSON.parse(jsonObj);
json = JSON.stringify(parsed, null, 2);
} catch (e) {
return json.replace(/&/g, '&').replace(//g, '>');
}
}
json = json.replace(/&/g, '&').replace(//g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
let cls = 'color: var(--accent-strong);';
if (/^"/.test(match)) {
if (/:$/.test(match)) cls = 'color: var(--ink); font-weight: 600;';
else cls = 'color: var(--pos);';
} else if (/true|false/.test(match)) {
cls = 'color: #d946ef; font-weight: 600;';
} else if (/null/.test(match)) {
cls = 'color: var(--ink-3); font-weight: 600;';
}
return '' + match + '';
});
}
// SVG Icons
const IconFile = (p) => ;
const IconRocket = (p) => ;
const IconMonitorPlay = (p) => ;
const IconChevronLeft = (p) => ;
const IconChevronRight = (p) => ;
const IconX = (p) => ;
const IconClock = (p) => ;
const PROMPTS_CONFIG = {
'1': {
title: '1. เช็กสุขภาพบัญชีด่วน', badge: 'READ', icon: IconActivity,
instruction: (d) => `เช็กสุขภาพบัญชีใน 5 นาที (read-only)\n\nดึงข้อมูล campaign ในช่วง ${d.timeframe?.start || ''} ถึง ${d.timeframe?.end || ''}\nสรุป 3 อย่าง: 1. ทำงานดี 2. ไม่คุ้ม 3. data น้อยไป`,
inputs: [{ type: 'daterange', id: 'timeframe', label: 'เลือกช่วงเวลา (Timeframe)' }]
},
'2': {
title: '2. หาเงินรั่ว', badge: 'READ', icon: IconCoins,
instruction: (d) => `หาเงินรั่วแบบอ่านง่าย (read-only)\n\nหา campaign ที่ใช้เงินสูงแต่ผลลัพธ์อ่อนในช่วง ${d.timeframe?.start || ''} ถึง ${d.timeframe?.end || ''}\nแสดงผลเป็นตาราง และสรุปปัญหา`,
inputs: [{ type: 'daterange', id: 'timeframe', label: 'เลือกช่วงเวลา (Timeframe)' }]
},
'3': {
title: '3. เช็ก Creative Fatigue', badge: 'READ', icon: IconImage,
instruction: (d) => `เช็ก creative fatigue (read-only)\nช่วง ${d.timeframe?.start || ''} ถึง ${d.timeframe?.end || ''}\nflag ad ที่ frequency เพิ่ม, CTR ลด, CPA แย่ลง`,
inputs: [{ type: 'daterange', id: 'timeframe', label: 'เลือกช่วงเวลาหลัก' }]
},
'4': {
title: '4. สรุปให้เจ้าของอ่าน', badge: 'READ', icon: IconFile,
instruction: (d) => `สรุปให้เจ้าของธุรกิจอ่าน (read-only)\n\nช่วง ${d.timeframe?.start || ''} ถึง ${d.timeframe?.end || ''}\nสรุป 4 ส่วน: อะไรเวิร์ก อะไรไม่คุ้ม อะไรยังพิจารณาไม่ได้ อะไรห้ามแตะ`,
inputs: [{ type: 'daterange', id: 'timeframe', label: 'เลือกช่วงเวลาที่ต้องการสรุป' }]
},
'5': {
title: '5. สร้าง Draft Campaign', badge: 'DRAFT', icon: IconPen,
instruction: (d) => `สร้าง campaign draft แบบ PAUSED\nสินค้า: ${d.product_name}\nLanding: ${d.landing_page}\nงบ: ${d.daily_budget}\nพื้นที่: ${d.location}\nอายุ: ${d.age}\nข้อความ: ${d.primary_text}\nหัวข้อ: ${d.headline}`,
inputs: [
{ type: 'text', id: 'product_name', label: 'สินค้า/บริการ', placeholder: 'เช่น คอร์สเรียนออนไลน์' },
{ type: 'text', id: 'landing_page', label: 'Landing Page URL', placeholder: 'https://...' },
{ type: 'number', id: 'daily_budget', label: 'งบต่อวัน (บาท)', placeholder: '500' },
{ type: 'text', id: 'location', label: 'พื้นที่ยิงโฆษณา', placeholder: 'ทั่วประเทศไทย' },
{ type: 'text', id: 'age', label: 'ช่วงอายุ', placeholder: '25-45' },
{ type: 'textarea', id: 'primary_text', label: 'ข้อความหลัก', placeholder: 'เขียนแคปชันโฆษณา...' },
{ type: 'text', id: 'headline', label: 'หัวข้อโฆษณา', placeholder: 'โปรโมชันพิเศษ...' }
]
},
'6': {
title: '6. อนุมัติเปิดยิง', badge: 'ACTION', icon: IconRocket,
instruction: (d) => `อนุมัติเปิดยิง (status change)\nCampaign ID = ${d.campaign_id}\nตรวจ 5 ข้อก่อนเปลี่ยน status`,
inputs: [{ type: 'text', id: 'campaign_id', label: 'Campaign ID', placeholder: 'ระบุ ID แคมเปญ' }]
},
'7': {
title: '7. ตรวจหลังยิงวันแรก', badge: 'READ', icon: IconSearch,
instruction: (d) => `ตรวจหลังเปิดยิงวันแรก (read-only)\nCampaign ID = ${d.campaign_id}\nสรุปผลลัพธ์ว่าปกติไหม ควรหยุดหรือไปต่อ`,
inputs: [{ type: 'text', id: 'campaign_id', label: 'Campaign ID', placeholder: 'ระบุ ID แคมเปญที่เพิ่งเปิดยิง' }]
},
'8': {
title: '8. สรุปยอดขายจาก Ads', badge: 'READ', icon: IconTrend,
instruction: (d) => `สรุปยอดขายจาก Ads ช่วง ${d.timeframe?.start || ''} ถึง ${d.timeframe?.end || ''}\nแยกตารางตามหมวดหมู่ แสดง Budget, Spend, ROAS, CPA, ยอดขาย เรียงจากตัวคุ้มสุดขึ้นก่อน`,
inputs: [{ type: 'daterange', id: 'timeframe', label: 'ช่วงเวลาที่ต้องการดู (Date Range)' }]
},
'logs': {
title: 'ประวัติการทำงาน (Logs)', badge: 'SYSTEM', icon: IconClock,
instruction: null,
inputs: [{ type: 'select', id: 'log_limit', label: 'จำนวนรายการ', options: ['10', '20', '50', '100'] }]
}
};
// ═══════════════════════════════════════════════════════════════
// Smart AI Text Parser for Slides
// ═══════════════════════════════════════════════════════════════
function parseAItoSlides(raw, title, dateRange) {
if (!raw) return [];
let text = typeof raw !== 'string' ? JSON.stringify(raw, null, 2) : raw;
try {
const parsed = JSON.parse(text);
if (typeof parsed === 'string') text = parsed;
else if (parsed.output) text = parsed.output;
} catch(e) {}
// Clean code blocks
text = text.replace(/```[a-z]*/gi, '').replace(/```/g, '');
// Detect if text has markdown tables
const hasTable = text.includes('|') && text.split('\n').some(l => l.trim().startsWith('|'));
// Parse tables into structured data
const parseTable = (lines) => {
const rows = [];
lines.forEach(l => {
const trimmed = l.trim();
if (!trimmed.startsWith('|') || trimmed.match(/^\|[-:\s|]+\|$/)) return; // skip separator
const cells = trimmed.split('|').filter(c => c.trim()).map(c => c.trim().replace(/\*\*/g, ''));
if (cells.length > 0) rows.push(cells);
});
return rows.length > 1 ? { headers: rows[0], data: rows.slice(1) } : null;
};
// Detect sentiment/status of a bullet
const getStatus = (text) => {
const lower = text.toLowerCase();
if (lower.match(/(good|positive|increase|growth|strong|high|success|profit|improve|effective|recommend|\u0e14\u0e35|\u0e40\u0e27\u0e34\u0e23\u0e4c\u0e01|\u0e01\u0e33\u0e44\u0e23|\u0e40\u0e1e\u0e34\u0e48\u0e21|\u0e2a\u0e39\u0e07|\u0e41\u0e19\u0e30\u0e19\u0e33|\u0e04\u0e38\u0e49\u0e21)/)) return 'good';
if (lower.match(/(bad|negative|decrease|decline|weak|low|fail|loss|poor|expensive|problem|issue|\u0e44\u0e21\u0e48\u0e04\u0e38\u0e49\u0e21|\u0e41\u0e1e\u0e07|\u0e25\u0e14|\u0e2b\u0e22\u0e38\u0e14|\u0e1b\u0e34\u0e14|\u0e2d\u0e31\u0e19\u0e15\u0e23\u0e32\u0e22|\u0e40\u0e07\u0e34\u0e19\u0e23\u0e31\u0e48\u0e27)/)) return 'bad';
if (lower.match(/(warning|caution|monitor|watch|uncertain|risk|\u0e23\u0e30\u0e27\u0e31\u0e07|\u0e15\u0e34\u0e14\u0e15\u0e32\u0e21|\u0e2a\u0e31\u0e07\u0e40\u0e01\u0e15|\u0e1e\u0e34\u0e08\u0e32\u0e23\u0e13\u0e32)/)) return 'warn';
return 'info';
};
const statusIcons = { good: '\u2705', bad: '\u274c', warn: '\u26a0\ufe0f', info: '\u2139\ufe0f' };
const statusColors = { good: '#34d399', bad: '#f87171', warn: '#fbbf24', info: '#60a5fa' };
// Parse into sections
const sections = [];
let current = { title: '', items: [], table: null };
let tableLines = [];
let inTable = false;
text.split('\n').forEach(line => {
const trimmed = line.trim();
if (!trimmed) {
if (inTable && tableLines.length > 0) {
current.table = parseTable(tableLines);
tableLines = [];
inTable = false;
}
return;
}
// Table line
if (trimmed.startsWith('|')) {
inTable = true;
tableLines.push(trimmed);
return;
} else if (inTable) {
current.table = parseTable(tableLines);
tableLines = [];
inTable = false;
}
const heading = trimmed.match(/^#{1,3}\s+(.+)/);
const numbered = trimmed.match(/^(\d+)\.\s+\*?\*?(.+?)\*?\*?\s*$/);
if (heading || numbered) {
if (current.title || current.items.length > 0 || current.table) sections.push({ ...current });
current = { title: (heading ? heading[1] : numbered[2]).replace(/\*\*/g, ''), items: [], table: null };
} else {
let clean = trimmed.replace(/^[-\u2022*]\s*/, '');
// Parse bold text: **text** -> keep but mark
let label = '';
let detail = clean;
const boldMatch = clean.match(/^\*\*(.+?)\*\*[:\s]*(.*)$/);
if (boldMatch) { label = boldMatch[1]; detail = boldMatch[2] || ''; }
else { detail = clean.replace(/\*\*/g, ''); }
// Extract numbers/currency
const numMatch = detail.match(/([\d,]+\.?\d*)\s*(THB|USD|\u0e1a\u0e32\u0e17|%|x)?/i);
if (clean) {
current.items.push({
label: label,
detail: detail,
status: getStatus(clean),
metric: numMatch ? numMatch[0] : null
});
}
}
});
if (inTable) current.table = parseTable(tableLines);
if (current.title || current.items.length > 0 || current.table) sections.push(current);
// Fallback
if (sections.length === 0) {
const lines = text.split('\n').filter(l => l.trim());
for (let i = 0; i < lines.length; i += 3) {
sections.push({ title: '', items: lines.slice(i, i + 3).map(l => ({ label: '', detail: l.replace(/^[-\u2022*]\s*/, '').replace(/\*\*/g, ''), status: 'info', metric: null })), table: null });
}
}
const bgs = [
'linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%)',
'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
'linear-gradient(135deg, #0d1117 0%, #161b22 50%, #21262d 100%)',
'linear-gradient(135deg, #1b2838 0%, #2a475e 100%)',
'linear-gradient(135deg, #0c0e1a 0%, #1a1f3a 50%, #2d1b69 100%)',
'linear-gradient(135deg, #0a192f 0%, #112240 100%)',
];
const slides = [{ type: 'title', title, dateRange, bg: bgs[0] }];
sections.forEach((sec, idx) => {
// Table slide
if (sec.table) {
slides.push({ type: 'table', title: sec.title, table: sec.table, bg: bgs[(idx + 1) % bgs.length] });
}
// Content slides (max 4 per slide for readability)
const max = 4;
if (sec.items.length > 0) {
if (sec.items.length <= max) {
slides.push({ type: 'content', title: sec.title, items: sec.items, bg: bgs[(idx + 1) % bgs.length] });
} else {
for (let i = 0; i < sec.items.length; i += max) {
slides.push({
type: 'content',
title: sec.title + (i > 0 ? ' (\u0e15\u0e48\u0e2d)' : ''),
items: sec.items.slice(i, i + max),
bg: bgs[(idx + 1 + i) % bgs.length]
});
}
}
}
});
slides.push({ type: 'end', bg: bgs[0] });
return slides;
}
// ═══════════════════════════════════════════════════════════════
// Main Component
// ═══════════════════════════════════════════════════════════════
function PromptsView({ onNavigate }) {
const [activeTab, setActiveTab] = useState('1');
const [formData, setFormData] = useState({});
const [loading, setLoading] = useState(false);
const [results, setResults] = useState({});
const [errorMsg, setErrorMsg] = useState('');
const [slideMode, setSlideMode] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
const config = PROMPTS_CONFIG[activeTab];
const n8nUrl = typeof getN8nSettings === 'function' ? getN8nSettings().hookPrompts : localStorage.getItem('n8nWebhookUrl');
const hasWebhook = Boolean(n8nUrl);
useEffect(() => {
const defaultData = {};
if (config?.inputs) {
config.inputs.forEach(inp => {
if (inp.type === 'daterange') defaultData[inp.id] = { start:'', end:'' };
else if (inp.type === 'select') defaultData[inp.id] = inp.options[0];
else defaultData[inp.id] = '';
});
}
setFormData(defaultData);
setErrorMsg('');
}, [activeTab]);
const handleInputChange = (id, value, isStart) => {
setFormData(prev => {
const updated = { ...prev };
if (isStart !== undefined) {
updated[id] = { ...updated[id], [isStart ? 'start' : 'end']: value };
} else {
updated[id] = value;
}
return updated;
});
};
const handleExportPdf = () => window.print();
const presRef = useRef(null);
const handleViewSlides = () => {
setCurrentSlide(0);
setSlideMode(true);
// Request real browser fullscreen after a tick (element needs to mount first)
setTimeout(() => {
const el = presRef.current || document.documentElement;
if (el.requestFullscreen) el.requestFullscreen().catch(() => {});
else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
}, 50);
};
const closeSlides = () => {
setSlideMode(false);
if (document.fullscreenElement || document.webkitFullscreenElement) {
if (document.exitFullscreen) document.exitFullscreen().catch(() => {});
else if (document.webkitExitFullscreen) document.webkitExitFullscreen();
}
};
// Sync state when user exits fullscreen via browser ESC
useEffect(() => {
const onFsChange = () => {
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
setSlideMode(false);
}
};
document.addEventListener('fullscreenchange', onFsChange);
document.addEventListener('webkitfullscreenchange', onFsChange);
return () => {
document.removeEventListener('fullscreenchange', onFsChange);
document.removeEventListener('webkitfullscreenchange', onFsChange);
};
}, []);
const handleClear = () => setResults(prev => ({...prev, [activeTab]: null}));
const handleSubmit = async (e) => {
e.preventDefault();
if (!hasWebhook) { setErrorMsg('ยังไม่ได้ตั้งค่า Webhook (ไปที่หน้า Settings)'); return; }
setLoading(true);
setErrorMsg('');
try {
if (activeTab === 'logs') {
const limit = formData.log_limit || 50;
const res = await fetch(`api.php?limit=${limit}`);
const data = await res.json();
setResults(prev => ({...prev, [activeTab]: (data.status === 'success' && data.data) ? data.data : []}));
} else {
const payload = {
promptId: activeTab,
instruction: typeof config.instruction === 'function' ? config.instruction(formData) : config.instruction,
formData,
timestamp: new Date().toISOString()
};
const response = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'proxy', webhookUrl: n8nUrl, payload })
});
const data = await response.json();
let output = data;
if (Array.isArray(data) && data.length > 0 && data[0].output) output = data[0].output;
else if (data.output) output = data.output;
setResults(prev => ({...prev, [activeTab]: output}));
}
} catch (err) { setErrorMsg(err.message); }
setLoading(false);
};
// Slides data
const slides = slideMode ? parseAItoSlides(results[activeTab], config.title, formData.timeframe ? formData.timeframe.start + ' \u2014 ' + formData.timeframe.end : null) : [];
const accentColors = ['#60a5fa', '#34d399', '#f472b6', '#a78bfa', '#fbbf24', '#fb923c'];
// Keyboard nav for slides
useEffect(() => {
if (!slideMode) return;
const handleKey = (e) => {
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); setCurrentSlide(s => Math.min(s + 1, slides.length - 1)); }
if (e.key === 'ArrowLeft') { e.preventDefault(); setCurrentSlide(s => Math.max(s - 1, 0)); }
if (e.key === 'Escape') closeSlides();
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [slideMode, slides.length]);
return (
{/* Header */}
{/* Navigation Sidebar */}
PROMPT COMMANDS
{Object.keys(PROMPTS_CONFIG).map(key => {
const p = PROMPTS_CONFIG[key];
const active = activeTab === key;
const badgeColors = { 'READ': 'var(--info)', 'DRAFT': 'var(--accent-strong)', 'ACTION': 'var(--neg)', 'SYSTEM': 'var(--ink-2)' };
return (
);
})}
{/* Main Panel */}
{/* Configuration Form */}
{/* AI Analysis Result */}
AI Analysis Result
{results[activeTab] && activeTab !== 'logs' && (
)}
{!results[activeTab] && !loading && (
{"\u0e40\u0e25\u0e37\u0e2d\u0e01\u0e04\u0e33\u0e2a\u0e31\u0e48\u0e07 \u0e01\u0e23\u0e2d\u0e01\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25 \u0e41\u0e25\u0e49\u0e27\u0e01\u0e14 \"\u0e23\u0e31\u0e19\u0e04\u0e33\u0e2a\u0e31\u0e48\u0e07\" \u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e14\u0e39\u0e1c\u0e25\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c"}
)}
{loading && (
{"\u0e01\u0e33\u0e25\u0e31\u0e07\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e1c\u0e48\u0e32\u0e19 n8n..."}
)}
{results[activeTab] && !loading && activeTab !== 'logs' && (
)}
{results[activeTab] && !loading && activeTab === 'logs' && (
{results['logs'].length === 0 &&
{"\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e1b\u0e23\u0e30\u0e27\u0e31\u0e15\u0e34\u0e01\u0e32\u0e23\u0e23\u0e31\u0e19"}
}
{results['logs'].map((log, idx) => (
Prompt {log.prompt_type}
{new Date(log.created_at).toLocaleString()}
))}
)}
{/* ═══════ PREMIUM PRESENTATION MODE (LANDSCAPE) ═══════ */}
{slideMode && slides.length > 0 && (
{/* Landscape rotation hint (mobile portrait) */}
{"\ud83d\udcf1\u21d4"}
{"\u0e01\u0e23\u0e38\u0e13\u0e32\u0e2b\u0e21\u0e38\u0e19\u0e08\u0e2d\u0e40\u0e1b\u0e47\u0e19\u0e41\u0e19\u0e27\u0e19\u0e2d\u0e19\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e14\u0e39 Presentation"}
{/* Progress bar */}
{/* Close button — fixed top-right corner */}
{/* Top info bar */}
AI Report
{"\u2022"}
{currentSlide + 1} / {slides.length}
{/* Slide content area */}
{/* TITLE SLIDE */}
{slides[currentSlide]?.type === 'title' && (
{slides[currentSlide].title}
{slides[currentSlide].dateRange && (
{slides[currentSlide].dateRange}
)}
{"\u0e43\u0e0a\u0e49 \u2190 \u2192 \u0e2b\u0e23\u0e37\u0e2d\u0e04\u0e25\u0e34\u0e01\u0e1b\u0e38\u0e48\u0e21\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e40\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e2a\u0e44\u0e25\u0e14\u0e4c"}
)}
{/* CONTENT SLIDE */}
{slides[currentSlide]?.type === 'content' && (() => {
const sColors = { good: '#34d399', bad: '#f87171', warn: '#fbbf24', info: '#60a5fa' };
const sIcons = { good: '✅', bad: '❌', warn: '⚠️', info: 'ℹ️' };
return (
{slides[currentSlide].title && (
{slides[currentSlide].title}
)}
{(slides[currentSlide].items || []).map((item, i) => {
const sc = sColors[item.status] || sColors.info;
return (
e.currentTarget.style.background = 'rgba(255,255,255,0.09)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.05)'}
>
{sIcons[item.status]}
{item.label &&
{item.label}
}
{item.detail}
{item.metric && (
{item.metric}
)}
);
})}
);
})()}
{/* TABLE SLIDE */}
{slides[currentSlide]?.type === 'table' && (
{slides[currentSlide].title && (
{slides[currentSlide].title}
)}
{slides[currentSlide].table.headers.map((h, i) => (
| {h} |
))}
{slides[currentSlide].table.data.map((row, ri) => (
{row.map((cell, ci) => (
| {cell} |
))}
))}
)}
{/* END SLIDE */}
{slides[currentSlide]?.type === 'end' && (
{"\u2705"}
{"\u0e08\u0e1a\u0e01\u0e32\u0e23\u0e19\u0e33\u0e40\u0e2a\u0e19\u0e2d"}
Powered by AI {"\u00d7"} Meta Ads Dashboard
)}
{/* Bottom controls */}
{slides.map((_, i) => (
)}
);
}
// Ensure IconBot exists
const IconBot = (p) => ;
Object.assign(window, { PromptsView });