193 lines
8.8 KiB
TypeScript
193 lines
8.8 KiB
TypeScript
import { CloseBotClient, err } from "../client.js";
|
|
import type { ToolDefinition, ToolResult, FileDto } from "../types.js";
|
|
|
|
export const tools: ToolDefinition[] = [
|
|
{
|
|
name: "library_manager_app",
|
|
description: "Knowledge base library viewer showing files with type icons, source attachments, scrape status, and size. Returns HTML visualization.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
fileId: { type: "string", description: "Optional: show detail for a specific file including scrape pages" },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
function fileIcon(fileType: string | null | undefined): string {
|
|
const t = (fileType || "").toLowerCase();
|
|
if (t.includes("pdf")) return "📄";
|
|
if (t.includes("doc") || t.includes("word")) return "📝";
|
|
if (t.includes("csv") || t.includes("excel") || t.includes("spreadsheet")) return "📊";
|
|
if (t.includes("image") || t.includes("png") || t.includes("jpg")) return "🖼️";
|
|
if (t.includes("webscrape") || t.includes("web") || t.includes("html")) return "🌐";
|
|
if (t.includes("text") || t.includes("txt")) return "📃";
|
|
if (t.includes("json")) return "🔧";
|
|
return "📁";
|
|
}
|
|
|
|
function formatSize(bytes: number): string {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function statusBadge(status: string | null | undefined): string {
|
|
const s = (status || "").toLowerCase();
|
|
if (s === "ready" || s === "completed" || s === "processed")
|
|
return '<span style="background:#1a3a2a;color:#4ecdc4;padding:2px 8px;border-radius:4px;font-size:10px;">✅ Ready</span>';
|
|
if (s === "processing" || s === "pending")
|
|
return '<span style="background:#3a3a1a;color:#ffd93d;padding:2px 8px;border-radius:4px;font-size:10px;">⏳ Processing</span>';
|
|
if (s === "error" || s === "failed")
|
|
return '<span style="background:#3a1a1a;color:#ff6b6b;padding:2px 8px;border-radius:4px;font-size:10px;">❌ Error</span>';
|
|
return `<span style="background:#1a1a2e;color:#888;padding:2px 8px;border-radius:4px;font-size:10px;">${escapeHtml(status || "Unknown")}</span>`;
|
|
}
|
|
|
|
function renderFileList(files: FileDto[]): string {
|
|
const totalSize = files.reduce((sum, f) => sum + (f.fileSize || 0), 0);
|
|
|
|
const rows = files
|
|
.map((f) => {
|
|
const icon = fileIcon(f.fileType);
|
|
const sources = (f.sources || [])
|
|
.map(
|
|
(s) =>
|
|
`<span style="background:#1a2a3e;padding:1px 6px;border-radius:4px;font-size:10px;color:#7ec8e3;">${escapeHtml(s.name || s.id || "")}</span>`
|
|
)
|
|
.join(" ");
|
|
const modified = f.lastModified
|
|
? new Date(f.lastModified).toLocaleDateString()
|
|
: "—";
|
|
|
|
return `
|
|
<div style="background:#1a1a2e;border-radius:10px;padding:14px;display:flex;align-items:center;gap:14px;">
|
|
<div style="font-size:28px;width:36px;text-align:center;">${icon}</div>
|
|
<div style="flex:1;min-width:0;">
|
|
<div style="font-weight:600;font-size:13px;color:#e0e0e0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
${escapeHtml(f.fileName || "Unnamed")}
|
|
</div>
|
|
<div style="display:flex;gap:8px;align-items:center;margin-top:4px;">
|
|
${statusBadge(f.fileStatus)}
|
|
<span style="font-size:11px;color:#666;">${escapeHtml(f.fileType || "")}</span>
|
|
<span style="font-size:11px;color:#666;">${formatSize(f.fileSize || 0)}</span>
|
|
<span style="font-size:11px;color:#666;">${escapeHtml(modified)}</span>
|
|
</div>
|
|
${sources ? `<div style="margin-top:6px;display:flex;gap:4px;flex-wrap:wrap;">${sources}</div>` : ""}
|
|
</div>
|
|
<div style="font-size:10px;color:#444;font-family:monospace;white-space:nowrap;">${escapeHtml(f.fileId || "")}</div>
|
|
</div>`;
|
|
})
|
|
.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;">📚 Library Manager</h2>
|
|
<div style="font-size:12px;color:#666;margin-bottom:16px;">
|
|
${files.length} files · ${formatSize(totalSize)} total
|
|
</div>
|
|
<div style="display:flex;flex-direction:column;gap:8px;">
|
|
${rows || '<div style="padding:40px;text-align:center;color:#666;">No files. Use upload_file or create_web_scrape to add content.</div>'}
|
|
</div>
|
|
<div style="margin-top:12px;font-size:11px;color:#555;">
|
|
💡 Use <code>upload_file</code> to upload, <code>create_web_scrape</code> to scrape websites, <code>attach_file_to_source</code> to connect to sources
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
function renderFileDetail(file: FileDto, scrapePages: Array<{ url?: string; enabled?: boolean }>): string {
|
|
const sources = (file.sources || [])
|
|
.map(
|
|
(s) =>
|
|
`<div style="background:#1a2a3e;padding:8px 12px;border-radius:6px;display:flex;justify-content:space-between;">
|
|
<span style="font-size:12px;color:#7ec8e3;">${escapeHtml(s.name || "Unnamed")}</span>
|
|
<span style="font-size:10px;color:#666;">${escapeHtml(s.category || "")} · ${escapeHtml(s.id || "")}</span>
|
|
</div>`
|
|
)
|
|
.join("");
|
|
|
|
const pages = scrapePages
|
|
.map(
|
|
(p) =>
|
|
`<div style="display:flex;align-items:center;gap:8px;padding:4px 0;">
|
|
<span style="font-size:14px;">${p.enabled ? "✅" : "❌"}</span>
|
|
<span style="font-size:12px;color:#aaa;word-break:break-all;">${escapeHtml(p.url || "")}</span>
|
|
</div>`
|
|
)
|
|
.join("");
|
|
|
|
return `
|
|
<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;background:#0d0d1a;color:#e0e0e0;padding:20px;border-radius:16px;">
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
|
|
<span style="font-size:36px;">${fileIcon(file.fileType)}</span>
|
|
<div>
|
|
<h2 style="margin:0;color:#fff;">${escapeHtml(file.fileName || "Unnamed")}</h2>
|
|
<div style="font-size:11px;color:#555;font-family:monospace;">${escapeHtml(file.fileId || "")}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;">
|
|
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
|
|
<div style="font-size:11px;color:#888;">Status</div>
|
|
<div style="margin-top:4px;">${statusBadge(file.fileStatus)}</div>
|
|
</div>
|
|
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
|
|
<div style="font-size:11px;color:#888;">Type</div>
|
|
<div style="font-size:13px;margin-top:4px;">${escapeHtml(file.fileType || "—")}</div>
|
|
</div>
|
|
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
|
|
<div style="font-size:11px;color:#888;">Size</div>
|
|
<div style="font-size:13px;margin-top:4px;">${formatSize(file.fileSize || 0)}</div>
|
|
</div>
|
|
<div style="background:#1a1a2e;padding:12px;border-radius:8px;text-align:center;">
|
|
<div style="font-size:11px;color:#888;">Modified</div>
|
|
<div style="font-size:13px;margin-top:4px;">${file.lastModified ? new Date(file.lastModified).toLocaleDateString() : "—"}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="color:#ccc;margin:16px 0 8px;">📡 Attached Sources (${file.sources?.length || 0})</h3>
|
|
<div style="display:flex;flex-direction:column;gap:4px;">
|
|
${sources || '<div style="color:#666;font-size:12px;">Not attached to any sources</div>'}
|
|
</div>
|
|
|
|
${scrapePages.length > 0 ? `
|
|
<h3 style="color:#ccc;margin:16px 0 8px;">🌐 Scrape Pages (${scrapePages.length})</h3>
|
|
<div style="background:#111;border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;">
|
|
${pages}
|
|
</div>
|
|
` : ""}
|
|
</div>`;
|
|
}
|
|
|
|
export async function handler(
|
|
client: CloseBotClient,
|
|
name: string,
|
|
args: Record<string, unknown>
|
|
): Promise<ToolResult> {
|
|
try {
|
|
if (args.fileId) {
|
|
const [file, scrapePages] = await Promise.all([
|
|
client.get<FileDto>(`/library/files/${args.fileId}`),
|
|
client.get<Array<{ url?: string; enabled?: boolean }>>(`/library/files/${args.fileId}/scrape-pages`).catch(() => []),
|
|
]);
|
|
const html = renderFileDetail(file, scrapePages);
|
|
return {
|
|
content: [{ type: "text", text: `File: ${file.fileName} (${file.fileType})` }],
|
|
structuredContent: { type: "html", html },
|
|
};
|
|
}
|
|
|
|
const files = await client.get<FileDto[]>("/library/files");
|
|
const html = renderFileList(files);
|
|
return {
|
|
content: [{ type: "text", text: `Library: ${files.length} files` }],
|
|
structuredContent: { type: "html", html },
|
|
};
|
|
} catch (error) {
|
|
return err(error);
|
|
}
|
|
}
|