623 lines
22 KiB
TypeScript
623 lines
22 KiB
TypeScript
/**
|
|
* <folk-campaign-wizard> — AI-guided step-by-step campaign creation.
|
|
*
|
|
* Steps: Brief → Structure → Content → Review → Activate
|
|
* Each step has Approve / Edit / Regenerate controls.
|
|
* Commits campaign, threads, newsletters, and workflow DAG on activation.
|
|
*/
|
|
|
|
type WizardStep = 'brief' | 'structure' | 'content' | 'review' | 'activate';
|
|
|
|
interface ExtractedBrief {
|
|
title: string;
|
|
audience: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
platforms: string[];
|
|
tone: string;
|
|
style: string;
|
|
keyMessages: string[];
|
|
}
|
|
|
|
interface CampaignStructurePhase {
|
|
name: string;
|
|
label: string;
|
|
days: string;
|
|
platforms: string[];
|
|
cadence: Record<string, number>;
|
|
}
|
|
|
|
interface CampaignStructure {
|
|
phases: CampaignStructurePhase[];
|
|
summary: string;
|
|
}
|
|
|
|
interface CampaignPost {
|
|
id: string;
|
|
platform: string;
|
|
postType: string;
|
|
stepNumber: number;
|
|
content: string;
|
|
scheduledAt: string;
|
|
status: string;
|
|
hashtags: string[];
|
|
phase: number;
|
|
phaseLabel: string;
|
|
threadPosts?: string[];
|
|
emailSubject?: string;
|
|
emailHtml?: string;
|
|
}
|
|
|
|
interface Campaign {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
duration: string;
|
|
platforms: string[];
|
|
phases: { name: string; label: string; days: string }[];
|
|
posts: CampaignPost[];
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
interface WizardState {
|
|
id: string;
|
|
step: WizardStep | 'committed' | 'abandoned';
|
|
rawBrief: string;
|
|
extractedBrief: ExtractedBrief | null;
|
|
structure: CampaignStructure | null;
|
|
campaignDraft: Campaign | null;
|
|
committedCampaignId: string | null;
|
|
}
|
|
|
|
const STEPS: { key: WizardStep; label: string; num: number }[] = [
|
|
{ key: 'brief', label: 'Brief', num: 1 },
|
|
{ key: 'structure', label: 'Structure', num: 2 },
|
|
{ key: 'content', label: 'Content', num: 3 },
|
|
{ key: 'review', label: 'Review', num: 4 },
|
|
{ key: 'activate', label: 'Activate', num: 5 },
|
|
];
|
|
|
|
const STEP_ORDER: Record<string, number> = { brief: 0, structure: 1, content: 2, review: 3, activate: 4, committed: 5, abandoned: -1 };
|
|
|
|
const PLATFORM_ICONS: Record<string, string> = {
|
|
x: '\uD83D\uDC26', linkedin: '\uD83D\uDCBC', instagram: '\uD83D\uDCF7',
|
|
threads: '\uD83E\uDDF5', bluesky: '\u2601\uFE0F', youtube: '\u25B6\uFE0F',
|
|
newsletter: '\uD83D\uDCE7',
|
|
};
|
|
|
|
export class FolkCampaignWizard extends HTMLElement {
|
|
private _space = 'demo';
|
|
private _wizardId = '';
|
|
private _step: WizardStep = 'brief';
|
|
private _loading = false;
|
|
private _error = '';
|
|
private _rawBrief = '';
|
|
private _extractedBrief: ExtractedBrief | null = null;
|
|
private _structure: CampaignStructure | null = null;
|
|
private _campaignDraft: Campaign | null = null;
|
|
private _committedCampaignId: string | null = null;
|
|
private _commitResult: any = null;
|
|
private _expandedPosts: Set<number> = new Set();
|
|
private _regenIndex = -1;
|
|
private _regenInstructions = '';
|
|
|
|
static get observedAttributes() { return ['space', 'wizard-id']; }
|
|
|
|
connectedCallback() {
|
|
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
|
|
this._space = this.getAttribute('space') || 'demo';
|
|
this._wizardId = this.getAttribute('wizard-id') || '';
|
|
|
|
if (this._wizardId) {
|
|
this.loadWizard();
|
|
} else {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
attributeChangedCallback(name: string, _old: string, val: string) {
|
|
if (name === 'space') this._space = val;
|
|
if (name === 'wizard-id' && val !== this._wizardId) {
|
|
this._wizardId = val;
|
|
if (val) this.loadWizard();
|
|
}
|
|
}
|
|
|
|
private get basePath(): string {
|
|
return `/${encodeURIComponent(this._space)}/rsocials`;
|
|
}
|
|
|
|
private async apiFetch(path: string, opts: RequestInit = {}): Promise<Response> {
|
|
const token = (window as any).__authToken || localStorage.getItem('auth_token') || '';
|
|
return fetch(`${this.basePath}${path}`, {
|
|
...opts,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(opts.headers || {}),
|
|
},
|
|
});
|
|
}
|
|
|
|
private async loadWizard(): Promise<void> {
|
|
this._loading = true;
|
|
this.render();
|
|
|
|
try {
|
|
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}`);
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Failed to load wizard');
|
|
const data: WizardState = await res.json();
|
|
|
|
this._rawBrief = data.rawBrief || '';
|
|
this._extractedBrief = data.extractedBrief;
|
|
this._structure = data.structure;
|
|
this._campaignDraft = data.campaignDraft;
|
|
this._committedCampaignId = data.committedCampaignId;
|
|
|
|
if (data.step === 'committed') {
|
|
this._step = 'activate';
|
|
} else if (data.step === 'abandoned') {
|
|
this._step = 'brief';
|
|
} else {
|
|
this._step = data.step as WizardStep;
|
|
}
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
}
|
|
this._loading = false;
|
|
this.render();
|
|
}
|
|
|
|
private async createWizard(): Promise<string | null> {
|
|
try {
|
|
const res = await this.apiFetch('/api/campaign/wizard', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ rawBrief: this._rawBrief }),
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Failed to create wizard');
|
|
const data = await res.json();
|
|
this._wizardId = data.wizardId;
|
|
|
|
// Update URL without reload
|
|
const url = `${this.basePath}/campaign-wizard/${data.wizardId}`;
|
|
history.replaceState(null, '', url);
|
|
|
|
return data.wizardId;
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
this.render();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Step actions ──
|
|
|
|
private async analyzebrief(): Promise<void> {
|
|
if (this._rawBrief.trim().length < 10) {
|
|
this._error = 'Brief must be at least 10 characters';
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
this._loading = true;
|
|
this._error = '';
|
|
this.render();
|
|
|
|
if (!this._wizardId) {
|
|
const id = await this.createWizard();
|
|
if (!id) { this._loading = false; this.render(); return; }
|
|
}
|
|
|
|
try {
|
|
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/structure`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ rawBrief: this._rawBrief }),
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Analysis failed');
|
|
const data: WizardState = await res.json();
|
|
this._extractedBrief = data.extractedBrief;
|
|
this._structure = data.structure;
|
|
this._step = 'structure';
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
}
|
|
this._loading = false;
|
|
this.render();
|
|
}
|
|
|
|
private async generateContent(): Promise<void> {
|
|
this._loading = true;
|
|
this._error = '';
|
|
this.render();
|
|
|
|
try {
|
|
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/content`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({}),
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Generation failed');
|
|
const data: WizardState = await res.json();
|
|
this._campaignDraft = data.campaignDraft;
|
|
this._step = 'content';
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
}
|
|
this._loading = false;
|
|
this.render();
|
|
}
|
|
|
|
private async regenPost(index: number): Promise<void> {
|
|
this._regenIndex = index;
|
|
this._loading = true;
|
|
this._error = '';
|
|
this.render();
|
|
|
|
try {
|
|
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/regen-post`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ postIndex: index, instructions: this._regenInstructions }),
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Regeneration failed');
|
|
const newPost: CampaignPost = await res.json();
|
|
if (this._campaignDraft) {
|
|
this._campaignDraft.posts[index] = { ...this._campaignDraft.posts[index], ...newPost };
|
|
}
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
}
|
|
this._regenIndex = -1;
|
|
this._regenInstructions = '';
|
|
this._loading = false;
|
|
this.render();
|
|
}
|
|
|
|
private async commitCampaign(): Promise<void> {
|
|
this._loading = true;
|
|
this._error = '';
|
|
this.render();
|
|
|
|
try {
|
|
const res = await this.apiFetch(`/api/campaign/wizard/${this._wizardId}/commit`, {
|
|
method: 'POST',
|
|
});
|
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || 'Commit failed');
|
|
this._commitResult = await res.json();
|
|
this._committedCampaignId = this._commitResult.campaignId;
|
|
this._step = 'activate';
|
|
} catch (e: any) {
|
|
this._error = e.message;
|
|
}
|
|
this._loading = false;
|
|
this.render();
|
|
}
|
|
|
|
private async abandonWizard(): Promise<void> {
|
|
if (!this._wizardId) return;
|
|
await this.apiFetch(`/api/campaign/wizard/${this._wizardId}`, { method: 'DELETE' });
|
|
window.location.href = `${this.basePath}/campaign-wizard`;
|
|
}
|
|
|
|
// ── Render ──
|
|
|
|
private render(): void {
|
|
if (!this.shadowRoot) return;
|
|
this.shadowRoot.innerHTML = `
|
|
<link rel="stylesheet" href="/modules/rsocials/campaign-wizard.css">
|
|
<div class="cw-container">
|
|
${this.renderHeader()}
|
|
${this.renderStepIndicator()}
|
|
${this._loading && this._step !== 'review' ? this.renderLoading() : this.renderCurrentStep()}
|
|
${this._error ? `<div class="cw-panel" style="border-color:#dc2626;color:#f87171">${this.escHtml(this._error)}</div>` : ''}
|
|
</div>
|
|
`;
|
|
this.bindEvents();
|
|
}
|
|
|
|
private renderHeader(): string {
|
|
return `<div class="cw-header">
|
|
<h1>Campaign Wizard</h1>
|
|
<p>AI-guided campaign creation with step-by-step approval</p>
|
|
</div>`;
|
|
}
|
|
|
|
private renderStepIndicator(): string {
|
|
const current = STEP_ORDER[this._step] ?? 0;
|
|
const committed = this._committedCampaignId != null;
|
|
|
|
const items = STEPS.map((s, i) => {
|
|
const isDone = committed || current > i;
|
|
const isActive = !committed && current === i;
|
|
const cls = isDone ? 'done' : isActive ? 'active' : '';
|
|
const icon = isDone ? '\u2713' : String(s.num);
|
|
return `
|
|
<li class="cw-step ${cls}">
|
|
<span class="cw-step__num">${icon}</span>
|
|
<span>${s.label}</span>
|
|
</li>
|
|
${i < STEPS.length - 1 ? `<li class="cw-step__line" style="background:${isDone ? 'var(--rs-accent,#14b8a6)' : 'var(--rs-border,#333)'}"></li>` : ''}
|
|
`;
|
|
}).join('');
|
|
|
|
return `<ul class="cw-steps">${items}</ul>`;
|
|
}
|
|
|
|
private renderLoading(): string {
|
|
const labels: Record<string, string> = {
|
|
brief: 'Analyzing your brief...',
|
|
structure: 'Generating campaign structure...',
|
|
content: 'Generating posts for each platform...',
|
|
review: 'Regenerating post...',
|
|
activate: 'Committing campaign...',
|
|
};
|
|
return `<div class="cw-loading"><div class="cw-spinner"></div><span>${labels[this._step] || 'Working...'}</span></div>`;
|
|
}
|
|
|
|
private renderCurrentStep(): string {
|
|
if (this._committedCampaignId) return this.renderActivateStep();
|
|
|
|
switch (this._step) {
|
|
case 'brief': return this.renderBriefStep();
|
|
case 'structure': return this.renderStructureStep();
|
|
case 'content': return this.renderContentStep();
|
|
case 'review': return this.renderReviewStep();
|
|
case 'activate': return this.renderActivateStep();
|
|
default: return this.renderBriefStep();
|
|
}
|
|
}
|
|
|
|
private renderBriefStep(): string {
|
|
return `<div class="cw-panel">
|
|
<h2>Step 1: Paste Your Campaign Brief</h2>
|
|
<p class="cw-hint">Paste raw text describing your event, product launch, or campaign goals. The AI will extract key details and propose a structure.</p>
|
|
<textarea class="cw-textarea" id="brief-input" placeholder="Example: We're launching MycoFi at ETHDenver on March 25, 2026. Target audience is web3 developers and DeFi enthusiasts. We want to build hype for 2 weeks before, have a strong presence during the 3-day event, and follow up for a week after...">${this.escHtml(this._rawBrief)}</textarea>
|
|
<div class="cw-actions">
|
|
<button class="cw-btn cw-btn--primary" id="analyze-btn" ${this._rawBrief.trim().length < 10 ? 'disabled' : ''}>Analyze Brief</button>
|
|
<button class="cw-btn cw-btn--ghost" id="mi-btn">Refine with MI</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
private renderStructureStep(): string {
|
|
const brief = this._extractedBrief;
|
|
const structure = this._structure;
|
|
if (!brief || !structure) return '<div class="cw-panel">No structure data. Go back to Step 1.</div>';
|
|
|
|
const briefFields = `
|
|
<div class="cw-brief-summary">
|
|
<div class="cw-brief-field"><div class="cw-brief-field__label">Title</div><div class="cw-brief-field__value">${this.escHtml(brief.title)}</div></div>
|
|
<div class="cw-brief-field"><div class="cw-brief-field__label">Audience</div><div class="cw-brief-field__value">${this.escHtml(brief.audience)}</div></div>
|
|
<div class="cw-brief-field"><div class="cw-brief-field__label">Dates</div><div class="cw-brief-field__value">${this.escHtml(brief.startDate)} to ${this.escHtml(brief.endDate)}</div></div>
|
|
<div class="cw-brief-field"><div class="cw-brief-field__label">Tone / Style</div><div class="cw-brief-field__value">${this.escHtml(brief.tone)} / ${this.escHtml(brief.style)}</div></div>
|
|
<div class="cw-brief-field"><div class="cw-brief-field__label">Platforms</div><div class="cw-brief-field__value">${brief.platforms.map(p => `${PLATFORM_ICONS[p] || ''} ${p}`).join(', ')}</div></div>
|
|
<div class="cw-brief-field cw-brief-field--wide"><div class="cw-brief-field__label">Key Messages</div><div class="cw-brief-field__value">${brief.keyMessages.map(m => `<div>- ${this.escHtml(m)}</div>`).join('')}</div></div>
|
|
</div>
|
|
`;
|
|
|
|
const phases = structure.phases.map(p => `
|
|
<div class="cw-phase-card">
|
|
<div class="cw-phase-card__header">
|
|
<span class="cw-phase-card__name">${this.escHtml(p.label)}</span>
|
|
<span class="cw-phase-card__days">${this.escHtml(p.days)}</span>
|
|
</div>
|
|
<div class="cw-phase-card__cadence">
|
|
${Object.entries(p.cadence || {}).map(([plat, count]) =>
|
|
`<span class="cw-cadence-badge">${PLATFORM_ICONS[plat] || ''} ${plat}: ${count} post${count !== 1 ? 's' : ''}</span>`
|
|
).join('')}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="cw-panel">
|
|
<h2>Step 2: Review Campaign Structure</h2>
|
|
<p class="cw-hint">${this.escHtml(structure.summary)}</p>
|
|
${briefFields}
|
|
</div>
|
|
<div class="cw-panel">
|
|
<h2>Proposed Phases</h2>
|
|
<div class="cw-phases">${phases}</div>
|
|
<div class="cw-actions">
|
|
<button class="cw-btn cw-btn--primary" id="approve-structure-btn">Approve & Generate Content</button>
|
|
<button class="cw-btn" id="regen-structure-btn">Regenerate Structure</button>
|
|
<button class="cw-btn cw-btn--ghost" id="back-to-brief-btn">Back to Brief</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderContentStep(): string {
|
|
if (!this._campaignDraft) return '<div class="cw-panel">No content generated yet.</div>';
|
|
|
|
const campaign = this._campaignDraft;
|
|
const postsByPhase = new Map<number, CampaignPost[]>();
|
|
for (const post of campaign.posts) {
|
|
const arr = postsByPhase.get(post.phase) || [];
|
|
arr.push(post);
|
|
postsByPhase.set(post.phase, arr);
|
|
}
|
|
|
|
const phaseSections = campaign.phases.map((phase, pi) => {
|
|
const posts = postsByPhase.get(pi + 1) || [];
|
|
const postCards = posts.map(p => `
|
|
<div class="cw-phase-card" style="margin-bottom:0.5rem">
|
|
<div class="cw-phase-card__header">
|
|
<span><span class="cw-platform-badge cw-platform-badge--${p.platform}">${PLATFORM_ICONS[p.platform] || ''} ${p.platform}</span> ${this.escHtml(p.postType)}</span>
|
|
<span class="cw-phase-card__days">${p.scheduledAt?.split('T')[0] || ''}</span>
|
|
</div>
|
|
<div class="cw-post-content cw-post-content--truncated">${this.escHtml(p.content)}</div>
|
|
${p.threadPosts?.length ? `<div style="font-size:0.75rem;color:var(--rs-text-muted,#64748b);margin-top:0.25rem">${p.threadPosts.length} tweets in thread</div>` : ''}
|
|
${p.hashtags?.length ? `<div style="font-size:0.75rem;color:var(--rs-accent,#14b8a6);margin-top:0.25rem">#${p.hashtags.join(' #')}</div>` : ''}
|
|
</div>
|
|
`).join('');
|
|
|
|
return `
|
|
<div class="cw-panel">
|
|
<h2>${this.escHtml(phase.label)} <span style="font-weight:400;font-size:0.85rem;color:var(--rs-text-muted,#64748b)">(${this.escHtml(phase.days)})</span></h2>
|
|
<div class="cw-phases">${postCards}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
return `
|
|
<div class="cw-panel">
|
|
<h2>Step 3: Generated Content</h2>
|
|
<p class="cw-hint">${campaign.posts.length} posts across ${campaign.platforms.length} platforms in ${campaign.phases.length} phases</p>
|
|
</div>
|
|
${phaseSections}
|
|
<div class="cw-actions" style="margin-bottom:1.5rem">
|
|
<button class="cw-btn cw-btn--primary" id="approve-content-btn">Proceed to Review</button>
|
|
<button class="cw-btn" id="regen-content-btn">Regenerate All Content</button>
|
|
<button class="cw-btn cw-btn--ghost" id="back-to-structure-btn">Back to Structure</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderReviewStep(): string {
|
|
if (!this._campaignDraft) return '<div class="cw-panel">No campaign to review.</div>';
|
|
|
|
const campaign = this._campaignDraft;
|
|
const rows = campaign.posts.map((p, i) => {
|
|
const isExpanded = this._expandedPosts.has(i);
|
|
const isRegening = this._regenIndex === i && this._loading;
|
|
const contentCls = isExpanded ? 'cw-post-content' : 'cw-post-content cw-post-content--truncated';
|
|
|
|
return `<tr>
|
|
<td>${p.stepNumber}</td>
|
|
<td><span class="cw-platform-badge cw-platform-badge--${p.platform}">${PLATFORM_ICONS[p.platform] || ''} ${p.platform}</span></td>
|
|
<td>${this.escHtml(p.phaseLabel)}</td>
|
|
<td>
|
|
<div class="${contentCls}" data-expand="${i}" style="cursor:pointer">${this.escHtml(p.content)}</div>
|
|
${p.threadPosts?.length ? `<div style="font-size:0.7rem;color:var(--rs-text-muted)">${p.threadPosts.length} tweets</div>` : ''}
|
|
${p.emailSubject ? `<div style="font-size:0.7rem;color:var(--rs-text-muted)">Subject: ${this.escHtml(p.emailSubject)}</div>` : ''}
|
|
</td>
|
|
<td>${p.scheduledAt?.split('T')[0] || '-'}</td>
|
|
<td>
|
|
${isRegening
|
|
? '<div class="cw-spinner" style="width:16px;height:16px"></div>'
|
|
: `<button class="cw-btn" style="padding:0.3rem 0.6rem;font-size:0.75rem" data-regen="${i}">Regen</button>`
|
|
}
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
return `
|
|
<div class="cw-panel">
|
|
<h2>Step 4: Review All Posts</h2>
|
|
<p class="cw-hint">Click any post content to expand. Use Regen to regenerate individual posts.</p>
|
|
<div style="overflow-x:auto">
|
|
<table class="cw-posts-table">
|
|
<thead><tr>
|
|
<th>#</th><th>Platform</th><th>Phase</th><th>Content</th><th>Scheduled</th><th></th>
|
|
</tr></thead>
|
|
<tbody>${rows}</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="cw-actions">
|
|
<button class="cw-btn cw-btn--primary" id="commit-btn">Activate Campaign</button>
|
|
<button class="cw-btn cw-btn--ghost" id="back-to-content-btn">Back to Content</button>
|
|
<button class="cw-btn cw-btn--danger" id="abandon-btn">Abandon Wizard</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderActivateStep(): string {
|
|
if (this._committedCampaignId) {
|
|
const result = this._commitResult || {};
|
|
return `<div class="cw-panel">
|
|
<div class="cw-success">
|
|
<div class="cw-success__icon">\u2705</div>
|
|
<h2>Campaign Activated!</h2>
|
|
<p style="color:var(--rs-text-secondary,#94a3b8)">Your campaign has been committed successfully.</p>
|
|
${result.threadIds?.length ? `<p style="font-size:0.85rem;color:var(--rs-text-muted,#64748b)">${result.threadIds.length} thread(s) created</p>` : ''}
|
|
${result.newsletters?.length ? `<p style="font-size:0.85rem;color:var(--rs-text-muted,#64748b)">${result.newsletters.length} newsletter draft(s)</p>` : ''}
|
|
<div class="cw-success__links">
|
|
<a href="${this.basePath}/campaign">View Campaign</a>
|
|
<a href="${this.basePath}/threads">View Threads</a>
|
|
<a href="${this.basePath}/campaigns?workflow=${result.workflowId || ''}">View Workflow</a>
|
|
<a href="${this.basePath}/campaign-wizard">New Wizard</a>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `<div class="cw-panel">
|
|
<h2>Step 5: Activate</h2>
|
|
<p class="cw-hint">Click Activate to commit the campaign, create threads, draft newsletters, and build the workflow.</p>
|
|
<div class="cw-actions">
|
|
<button class="cw-btn cw-btn--primary" id="commit-btn">Activate Campaign</button>
|
|
<button class="cw-btn cw-btn--ghost" id="back-to-review-btn">Back to Review</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ── Event binding ──
|
|
|
|
private bindEvents(): void {
|
|
const sr = this.shadowRoot!;
|
|
|
|
// Brief step
|
|
const briefInput = sr.querySelector('#brief-input') as HTMLTextAreaElement | null;
|
|
if (briefInput) {
|
|
briefInput.addEventListener('input', () => {
|
|
this._rawBrief = briefInput.value;
|
|
const btn = sr.querySelector('#analyze-btn') as HTMLButtonElement | null;
|
|
if (btn) btn.disabled = this._rawBrief.trim().length < 10;
|
|
});
|
|
}
|
|
|
|
sr.querySelector('#analyze-btn')?.addEventListener('click', () => this.analyzebrief());
|
|
sr.querySelector('#mi-btn')?.addEventListener('click', () => {
|
|
this.dispatchEvent(new CustomEvent('mi-prompt', {
|
|
bubbles: true, composed: true,
|
|
detail: { prompt: `Help me refine this campaign brief:\n\n${this._rawBrief}` },
|
|
}));
|
|
});
|
|
|
|
// Structure step
|
|
sr.querySelector('#approve-structure-btn')?.addEventListener('click', () => this.generateContent());
|
|
sr.querySelector('#regen-structure-btn')?.addEventListener('click', () => this.analyzebrief());
|
|
sr.querySelector('#back-to-brief-btn')?.addEventListener('click', () => { this._step = 'brief'; this.render(); });
|
|
|
|
// Content step
|
|
sr.querySelector('#approve-content-btn')?.addEventListener('click', () => { this._step = 'review'; this.render(); });
|
|
sr.querySelector('#regen-content-btn')?.addEventListener('click', () => this.generateContent());
|
|
sr.querySelector('#back-to-structure-btn')?.addEventListener('click', () => { this._step = 'structure'; this.render(); });
|
|
|
|
// Review step — expand and regen
|
|
sr.querySelectorAll('[data-expand]').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
const idx = parseInt(el.getAttribute('data-expand')!);
|
|
if (this._expandedPosts.has(idx)) this._expandedPosts.delete(idx);
|
|
else this._expandedPosts.add(idx);
|
|
this.render();
|
|
});
|
|
});
|
|
sr.querySelectorAll('[data-regen]').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
const idx = parseInt(el.getAttribute('data-regen')!);
|
|
this.regenPost(idx);
|
|
});
|
|
});
|
|
|
|
// Commit / back / abandon
|
|
sr.querySelector('#commit-btn')?.addEventListener('click', () => this.commitCampaign());
|
|
sr.querySelector('#back-to-content-btn')?.addEventListener('click', () => { this._step = 'content'; this.render(); });
|
|
sr.querySelector('#back-to-review-btn')?.addEventListener('click', () => { this._step = 'review'; this.render(); });
|
|
sr.querySelector('#abandon-btn')?.addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to abandon this wizard? All progress will be lost.')) {
|
|
this.abandonWizard();
|
|
}
|
|
});
|
|
}
|
|
|
|
private escHtml(s: string): string {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
}
|
|
|
|
customElements.define('folk-campaign-wizard', FolkCampaignWizard);
|