185 lines
8.6 KiB
TypeScript

import { CloseBotClient, err } from "../client.js";
import type { ToolDefinition, ToolResult, LeadDto, LeadDtoPaginated } from "../types.js";
export const tools: ToolDefinition[] = [
{
name: "lead_manager_app",
description: "Searchable lead table with fields, conversation snippets, and status indicators. Returns HTML visualization.",
inputSchema: {
type: "object",
properties: {
sourceId: { type: "string", description: "Filter by source ID" },
page: { type: "number", description: "Page number (0-indexed)" },
pageSize: { type: "number", description: "Page size (default 20, max 100)" },
leadId: { type: "string", description: "Show details for a specific lead" },
},
},
},
];
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function renderLeadTable(data: LeadDtoPaginated): string {
const leads = data.results || [];
const rows = leads
.map((lead) => {
const lastMsg = lead.lastMessage
? escapeHtml(lead.lastMessage.slice(0, 50))
: '<span style="color:#555;">—</span>';
const direction =
lead.lastMessageDirection === "outbound"
? '<span style="color:#4ecdc4;">→ out</span>'
: lead.lastMessageDirection === "inbound"
? '<span style="color:#ff9f43;">← in</span>'
: '<span style="color:#555;">—</span>';
const time = lead.lastMessageTime
? new Date(lead.lastMessageTime).toLocaleString()
: "—";
const source = lead.source?.name || "—";
const fieldCount = lead.fields?.length || 0;
const instanceCount = lead.instances?.length || 0;
const failReason = lead.mostRecentFailureReason
? `<div style="font-size:10px;color:#ff6b6b;margin-top:2px;">⚠️ ${escapeHtml(lead.mostRecentFailureReason.slice(0, 40))}</div>`
: "";
const tags =
lead.tags && lead.tags.length > 0
? lead.tags
.slice(0, 3)
.map((t) => `<span style="background:#2a1a3e;padding:1px 6px;border-radius:4px;font-size:10px;">${escapeHtml(t)}</span>`)
.join(" ")
: "";
return `
<tr style="border-bottom:1px solid #222;">
<td style="padding:10px 8px;">
<div style="font-weight:600;font-size:13px;">${escapeHtml(lead.name || "Unknown")}</div>
<div style="font-size:10px;color:#555;font-family:monospace;">${escapeHtml(lead.id || "")}</div>
${tags ? `<div style="margin-top:4px;">${tags}</div>` : ""}
</td>
<td style="padding:10px 8px;font-size:12px;color:#888;">${escapeHtml(source)}</td>
<td style="padding:10px 8px;font-size:12px;">
${direction}
<div style="font-size:11px;color:#aaa;margin-top:2px;">${lastMsg}</div>
${failReason}
</td>
<td style="padding:10px 8px;font-size:11px;color:#666;">${escapeHtml(time)}</td>
<td style="padding:10px 8px;text-align:center;font-size:12px;">${fieldCount}</td>
<td style="padding:10px 8px;text-align:center;font-size:12px;">${instanceCount}</td>
</tr>`;
})
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">👥 Lead Manager</h2>
<div style="font-size:12px;color:#666;margin-bottom:16px;">
${data.total ?? leads.length} total leads · Page ${(data.page ?? 0) + 1} · ${data.pageSize ?? 20} per page
</div>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#1a1a2e;">
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Lead</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Source</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Last Message</th>
<th style="text-align:left;padding:10px 8px;font-size:12px;color:#888;">Time</th>
<th style="text-align:center;padding:10px 8px;font-size:12px;color:#888;">Fields</th>
<th style="text-align:center;padding:10px 8px;font-size:12px;color:#888;">Bots</th>
</tr>
</thead>
<tbody>
${rows || '<tr><td colspan="6" style="padding:40px;text-align:center;color:#666;">No leads found</td></tr>'}
</tbody>
</table>
</div>
</div>`;
}
function renderLeadDetail(lead: LeadDto): string {
const fields = (lead.fields || [])
.map(
(f) =>
`<tr><td style="padding:4px 8px;color:#888;font-size:12px;">${escapeHtml(f.field || "")}</td><td style="padding:4px 8px;font-size:12px;">${escapeHtml(f.value || "—")}</td></tr>`
)
.join("");
const instances = (lead.instances || [])
.map(
(i) =>
`<tr><td style="padding:4px 8px;font-size:12px;font-family:monospace;">${escapeHtml(i.botId || "")}</td><td style="padding:4px 8px;font-size:12px;">v${escapeHtml(i.botVersion || "?")}</td><td style="padding:4px 8px;font-size:12px;">${i.followUpTime ? new Date(i.followUpTime).toLocaleString() : "—"}</td></tr>`
)
.join("");
const tags = (lead.tags || [])
.map((t) => `<span style="background:#2a1a3e;padding:2px 8px;border-radius:6px;font-size:11px;margin:2px;">${escapeHtml(t)}</span>`)
.join("");
return `
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
<h2 style="margin:0 0 4px;color:#fff;">👤 ${escapeHtml(lead.name || "Unknown Lead")}</h2>
<div style="font-size:12px;color:#555;font-family:monospace;margin-bottom:16px;">${escapeHtml(lead.id || "")}</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px;">
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Source</div>
<div style="font-size:14px;margin-top:4px;">${escapeHtml(lead.source?.name || "—")}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Last Message</div>
<div style="font-size:12px;margin-top:4px;color:#aaa;">${escapeHtml((lead.lastMessage || "—").slice(0, 80))}</div>
</div>
<div style="background:#1a1a2e;padding:12px;border-radius:8px;">
<div style="font-size:11px;color:#888;">Contact ID</div>
<div style="font-size:12px;margin-top:4px;font-family:monospace;">${escapeHtml(lead.contactId || "—")}</div>
</div>
</div>
${tags ? `<div style="margin-bottom:16px;">${tags}</div>` : ""}
${lead.mostRecentFailureReason ? `<div style="background:#2a1a1a;border:1px solid #5a2a2a;border-radius:8px;padding:12px;margin-bottom:16px;font-size:12px;color:#ff6b6b;">⚠️ ${escapeHtml(lead.mostRecentFailureReason)}</div>` : ""}
<h3 style="color:#ccc;margin:16px 0 8px;">📋 Fields (${lead.fields?.length || 0})</h3>
<table style="width:100%;border-collapse:collapse;">
${fields || '<tr><td style="padding:8px;color:#666;">No fields</td></tr>'}
</table>
<h3 style="color:#ccc;margin:16px 0 8px;">🤖 Bot Instances (${lead.instances?.length || 0})</h3>
<table style="width:100%;border-collapse:collapse;">
<tr style="background:#1a1a2e;"><th style="text-align:left;padding:6px 8px;font-size:11px;">Bot ID</th><th>Version</th><th>Follow-up</th></tr>
${instances || '<tr><td colspan="3" style="padding:8px;color:#666;">No instances</td></tr>'}
</table>
</div>`;
}
export async function handler(
client: CloseBotClient,
name: string,
args: Record<string, unknown>
): Promise<ToolResult> {
try {
if (args.leadId) {
const lead = await client.get<LeadDto>(`/lead/${args.leadId}`);
const html = renderLeadDetail(lead);
return {
content: [{ type: "text", text: `Lead details: ${lead.name || lead.id}` }],
structuredContent: { type: "html", html },
};
}
const data = await client.get<LeadDtoPaginated>("/lead", {
page: args.page,
pageSize: args.pageSize || 20,
sourceId: args.sourceId,
});
const html = renderLeadTable(data);
return {
content: [{ type: "text", text: `${data.total ?? (data.results?.length || 0)} leads found` }],
structuredContent: { type: "html", html },
};
} catch (error) {
return err(error);
}
}