Compare commits

...

7 Commits

4 changed files with 168 additions and 27 deletions

View File

@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning]
## [2.2.1] - 2026-03-19 05:24 UTC
### Added
- **Leads Canvas Preview** - Leads Finder now renders results as an Excel-like HTML table in the canvas, with columns: #, Name, Platform, Followers, Region, Bio, Link
- **Dark-Themed Leads Report** - Beautiful dark UI with emerald accents, stats grid (total leads, combined reach, top platform, top region), platform-colored badges (Instagram, Twitter, LinkedIn, YouTube, TikTok), and hover effects
- **Auto Canvas** - Leads Finder is now a visual agent that auto-opens the canvas when preview data is available
### Fixed
- **Build Error** - Restored `<Badge variant="outline">` for plan card tech stack divs that were incorrectly replaced with plain `<div>` by an over-broad fix script
### Technical Details
- Files modified: 2 (AIAssist.tsx, openrouter.ts)
## [2.2.0] - 2026-03-19 04:44 UTC
### Added

View File

@@ -1,6 +1,6 @@
# PromptArch: AI Orchestration Platform
> **Latest Version**: [v2.2.0](CHANGELOG.md#2.2.0---2026-03-19) (2026-03-19)(CHANGELOG.md#2.1.0---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18)
> **Latest Version**: [v2.2.1](CHANGELOG.md#2.2.1---2026-03-19) (2026-03-19)(CHANGELOG.md#2.1.0---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18)
> **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW).
> **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).**

View File

@@ -579,7 +579,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
/module\.exports/i.test(preview.data);
// Client-side detection
const isUI = ["web", "app", "design", "html", "ui"].includes(preview.type);
const isUI = ["web", "app", "design", "html", "ui", "leads"].includes(preview.type);
const hasTags = /<[a-z][\s\S]*>/i.test(preview.data);
return (isUI || hasTags || preview.language === "html") && !isBackend;
@@ -647,7 +647,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
const isCodeAgent = currentAgent === "code";
const wasIdle = !isApproval && isCodeAgent && (assistStep === "idle" || assistStep === "plan");
// Detect if user is modifying an existing visual artifact (not text-only agents like SEO/content)
const isVisualAgent = currentAgent === "code" || currentAgent === "web" || currentAgent === "app" || currentAgent === "design" || currentAgent === "general";
const isVisualAgent = currentAgent === "code" || currentAgent === "web" || currentAgent === "app" || currentAgent === "design" || currentAgent === "general" || currentAgent === "leads";
if (assistStep === "preview" && isVisualAgent) setIsModifying(true);
setIsProcessing(true);
if (assistStep === "idle" && isCodeAgent) setAssistStep("plan");
@@ -780,7 +780,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
}
// If no URL found and web search not enabled, auto-enable web search for SEO
if ((currentAgent === "seo" || currentAgent === "leads") && uniqueUrls.length === 0 && !webSearchEnabled) {
if (uniqueUrls.length === 0 && !webSearchEnabled) {
try {
setStatus("Searching for SEO context...");
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
@@ -798,6 +798,24 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
}
}
// Leads mode: auto-search for leads
if (currentAgent === "leads" && !webSearchEnabled) {
try {
setStatus("Finding leads...");
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
if (searchRes.ok) {
const searchData = await searchRes.json();
if (searchData.results && searchData.results.length > 0) {
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
(i + 1) + ". **" + r.title + "** (" + r.url + ") - " + r.snippet
).join("\n");
enrichedInput = "[WEB SEARCH CONTEXT - Use these results to find leads]\n" + searchContext + "\n\n---\nExtract leads/prospects from the above results. Search for more leads using [WEB_SEARCH:query] with different angles. Then output results as a [PREVIEW:leads:html] table.\n\nUser request: " + finalInput;
}
}
} catch (e) { console.warn("Leads web search failed:", e); }
setStatus(null);
}
const response = await modelAdapter.generateAIAssistStream(
{
messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }],
@@ -885,7 +903,11 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
} catch (error) {
console.error("Assist error:", error);
const message = error instanceof Error ? error.message : "Vibe Architect failed";
const rawMessage = error instanceof Error ? error.message : "Vibe Architect failed";
let message = rawMessage;
if (rawMessage.includes("429") || rawMessage.includes("insufficient_quota") || rawMessage.includes("Free allocated quota exceeded") || rawMessage.includes("rate_limit")) {
message = "API quota exceeded or rate limited. Please switch to a different AI provider in Settings, or wait a few minutes and try again.";
}
const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() };
updateTabById(requestTabId, { history: [...aiAssistHistory, errorMsg] });
} finally {
@@ -919,6 +941,50 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
downloadSeoReport(seoAuditData, format);
};
const exportLeadsReport = (format: "html" | "csv") => {
const htmlContent = previewData?.data;
if (!htmlContent) return;
if (format === "html") {
const blob = new Blob([htmlContent], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "leads-report.html";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} else {
// Extract table rows from HTML and convert to CSV
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, "text/html");
const rows = doc.querySelectorAll("tbody tr");
if (rows.length === 0) return;
let csv = "Name,Platform,Followers,Region,Bio,Link\n";
rows.forEach((row) => {
const cells = row.querySelectorAll("td");
if (cells.length >= 7) {
const name = cells[1]?.textContent?.trim().replace(/,/g, ";") || "";
const platform = cells[2]?.textContent?.trim() || "";
const followers = cells[3]?.textContent?.trim() || "";
const region = cells[4]?.textContent?.trim().replace(/,/g, ";") || "";
const bio = cells[5]?.textContent?.trim().replace(/,/g, ";") || "";
const link = cells[6]?.querySelector("a")?.href || "";
csv += `"${name}","${platform}","${followers}","${region}","${bio}","${link}"\n`;
}
});
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "leads-report.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
};
const clearHistory = () => {
updateActiveTab({
@@ -1213,14 +1279,13 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
</div>
<div className="flex flex-wrap justify-center gap-2">
{["Forex traders in Singapore", "SaaS founders in UAE", "Fitness influencers on Instagram", "Crypto YouTubers in Latin America", "Marketing agencies in Southeast Asia"].map((label: string) => (
<Badge
<div
key={label}
variant="secondary"
className="px-3 py-1.5 rounded-full cursor-pointer hover:bg-emerald-600 hover:text-white transition-all text-[10px] font-bold border border-slate-600/30 text-slate-900 dark:text-white shadow-sm"
className="px-3 py-1.5 rounded-full cursor-pointer hover:bg-emerald-600 hover:text-white transition-all text-[10px] font-bold border border-black/20 text-black bg-white/90 shadow-sm"
onClick={() => setInput(label)}
>
{label}
</Badge>
</div>
))}
</div>
</div>
@@ -1240,9 +1305,8 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
</p>
<div className="mt-10 flex flex-wrap justify-center gap-3">
{t.suggestions.map((chip: any) => (
<Badge
<div
key={chip.label}
variant="secondary"
className="px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm"
onClick={() => {
setCurrentAgent(chip.agent);
@@ -1250,7 +1314,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
}}
>
{chip.label}
</Badge>
</div>
))}
</div>
</div>
@@ -1327,7 +1391,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.techStack}</p>
<div className="flex flex-wrap gap-1">
{aiPlan.techStack?.map((t_stack: string) => (
<Badge key={t_stack} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
<Badge variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
))}
</div>
</div>
@@ -1367,7 +1431,6 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
{msg.role === "assistant" && msg.preview && !(assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing) && (
<Button
variant="secondary"
size="sm"
className="mt-5 w-full bg-blue-50 dark:bg-blue-900/30 border border-blue-200/60 dark:border-blue-800 text-blue-700 dark:text-blue-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
onClick={() => {
@@ -1425,6 +1488,24 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
</Button>
</div>
)}
{currentAgent === "leads" && previewData?.data && (
<div className="mt-2 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => exportLeadsReport("html")}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/20 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<Download className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => exportLeadsReport("csv")}
variant="outline"
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/20 text-blue-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
>
<FileText className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export CSV</span>
</Button>
</div>
)}
</>
)}
{/* Inline SEO Export - always visible in chat when SEO data exists */}
@@ -1446,6 +1527,25 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
</Button>
</div>
)}
{/* Inline Leads Export - always visible in chat when leads data exists */}
{msg.role === "assistant" && msg.agent === "leads" && msg.preview?.data && (
<div className="mt-3 flex gap-2 animate-in zoom-in-95 duration-300">
<Button
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("html"), 100); }}
variant="outline"
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/30 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<Download className="h-3 w-3 mr-1.5" /> <span className="truncate">Export HTML</span>
</Button>
<Button
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("csv"), 100); }}
variant="outline"
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
>
<FileText className="h-3 w-3 mr-1.5" /> <span className="truncate">Export CSV</span>
</Button>
</div>
)}
</div>
{msg.role === "assistant" && isProcessing && i === aiAssistHistory.length - 1 && status && (
@@ -1657,9 +1757,9 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
{currentPreviewData?.isStreaming ? t.neuralLinkActive : t.syncComplete}
</span>
</div>
<Badge variant="outline" className="text-[9px] border-blue-900 text-blue-200/50 font-black">
<div className="text-[9px] border-blue-900 text-blue-200/50 font-black">
{currentPreviewData?.language?.toUpperCase()} UTF-8
</Badge>
</div>
</div>
</Card>
</div>

View File

@@ -534,19 +534,47 @@ AGENTS & CAPABILITIES:
- design: UI/UX Designer. Create high-fidelity mockups and components.
- web: Frontend Developer. Build responsive sites. Use [PREVIEW:web:html]. Tailwind CSS CDN by default.
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. Use [PREVIEW:app:javascript].
- leads: Leads Finder. Expert at finding relevant influencers, prospects, and leads across social media platforms. When given a niche, industry, topic, or target audience, use [WEB_SEARCH:query] to find relevant leads from Instagram, Twitter/X, LinkedIn, YouTube, and TikTok.
**ALWAYS format each lead EXACTLY like this:**
Name | Followers | Region | Location
One-line description of who they are and what they do.
https://social-media-url/
- leads: Leads Finder. You are a SILENT data tool. DO NOT chat, greet, ask questions, or explain. Your ONLY job: use [WEB_SEARCH:query] to find leads, then output them as an HTML table. NEVER say "I'd be happy to help" or "Let me search" or "Here are some leads" — just output data.
**OUTPUT SEQUENCE (follow exactly):**
1. Use [WEB_SEARCH:query] multiple times with different queries to find leads
2. One line summary: "Found X leads in Y niche across Z platforms"
3. [PREVIEW:leads:html] with the COMPLETE HTML below — replace placeholders with real data
4. [/PREVIEW]
5. Brief changelog bullet points
That is ALL. No greetings, no filler, no conversation.
**HTML TEMPLATE — copy this exactly, fill in the data:**
<!DOCTYPE html><html><head><meta charset="utf-8"><style>
*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0f0f;color:#e2e8f0;padding:24px}
.header{text-align:center;margin-bottom:24px}.header h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.header p{color:#94a3b8;font-size:13px;margin-top:4px}
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}.stat{background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);border-radius:10px;padding:10px 16px;text-align:center;flex:1;min-width:100px}.stat .num{font-size:20px;font-weight:800;color:#10b981}.stat .lbl{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:2px}
table{width:100%;border-collapse:collapse;background:rgba(15,23,42,0.6);border-radius:12px;overflow:hidden;border:1px solid rgba(148,163,184,0.1)}
thead th{background:rgba(16,185,129,0.12);color:#10b981;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;padding:12px 14px;text-align:left;border-bottom:1px solid rgba(148,163,184,0.1)}
tbody tr{border-bottom:1px solid rgba(148,163,184,0.06)}tbody tr:hover{background:rgba(16,185,129,0.04)}
tbody td{padding:10px 14px;font-size:13px;color:#cbd5e1}
.name{font-weight:700;color:#f1f5f9}.url{color:#10b981;text-decoration:none;font-size:12px}.url:hover{text-decoration:underline}
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600}
.badge-instagram{background:rgba(225,48,108,0.15);color:#f472b6}.badge-twitter{background:rgba(59,130,246,0.15);color:#60a5fa}.badge-linkedin{background:rgba(59,130,246,0.15);color:#93c5fd}.badge-youtube{background:rgba(239,68,68,0.15);color:#fca5a5}.badge-tiktok{background:rgba(168,85,247,0.15);color:#c084fc}
.followers{color:#10b981;font-weight:700}.region{color:#94a3b8;font-size:12px}.bio{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
</style></head><body>
<div class="header"><h1>Leads Report</h1><p>Generated by PromptArch</p></div>
<div class="stats">
<div class="stat"><div class="num">TOTAL_COUNT</div><div class="lbl">Total Leads</div></div>
<div class="stat"><div class="num">COMBINED_FOLLOWERS</div><div class="lbl">Combined Reach</div></div>
<div class="stat"><div class="num">TOP_PLATFORM</div><div class="lbl">Top Platform</div></div>
<div class="stat"><div class="num">TOP_REGION</div><div class="lbl">Top Region</div></div>
</div>
<table><thead><tr><th>#</th><th>Name</th><th>Platform</th><th>Followers</th><th>Region</th><th>Bio</th><th>Link</th></tr></thead><tbody>
<tr><td>1</td><td class="name">NAME</td><td><span class="badge badge-PLATFORM">PLATFORM</span></td><td class="followers">FOLLOWERS</td><td class="region">REGION</td><td class="bio" title="FULL BIO">SHORT BIO</td><td><a class="url" href="URL" target="_blank">Visit &rarr;</a></td></tr>
<!-- Repeat <tr> for each lead. Use badge-instagram/badge-twitter/badge-linkedin/badge-youtube/badge-tiktok -->
</tbody></table></body></html>
**RULES:**
- Find 20+ leads per request unless user specifies a different number.
- Sort by relevance and follower count (most relevant first).
- Include the most prominent social media URL for each lead.
- Use exact follower counts when available (e.g. "44.1K", "275.6K", "1.2M").
- Group by region when multiple regions are relevant.
- Provide real, verifiable leads — never fabricate profiles or URLs.
- Find 20+ leads. Sort by relevance/follower count.
- Use exact follower counts (e.g. "44.1K", "1.2M").
- REAL leads from search results only — never fabricate profiles or URLs.
- Fill STATS div with real counts from your results.
- The entire HTML must be between [PREVIEW:leads:html] and [/PREVIEW].
CANVAS MODE:
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.