@nordsym/apiclaw 1.3.13 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/PRD-ANALYTICS-AGENTS-TEAMS.md +710 -0
  2. package/PRD-API-CHAINING.md +483 -0
  3. package/PRD-HARDEN-SHELL.md +18 -12
  4. package/PRD-LOGS-SUBAGENTS-V2.md +267 -0
  5. package/convex/_generated/api.d.ts +6 -0
  6. package/convex/agents.ts +188 -0
  7. package/convex/chains.ts +1248 -0
  8. package/convex/logs.ts +94 -0
  9. package/convex/schema.ts +139 -0
  10. package/convex/searchLogs.ts +141 -0
  11. package/convex/teams.ts +243 -0
  12. package/dist/chain-types.d.ts +187 -0
  13. package/dist/chain-types.d.ts.map +1 -0
  14. package/dist/chain-types.js +33 -0
  15. package/dist/chain-types.js.map +1 -0
  16. package/dist/chainExecutor.d.ts +122 -0
  17. package/dist/chainExecutor.d.ts.map +1 -0
  18. package/dist/chainExecutor.js +454 -0
  19. package/dist/chainExecutor.js.map +1 -0
  20. package/dist/chainResolver.d.ts +100 -0
  21. package/dist/chainResolver.d.ts.map +1 -0
  22. package/dist/chainResolver.js +519 -0
  23. package/dist/chainResolver.js.map +1 -0
  24. package/dist/chainResolver.test.d.ts +5 -0
  25. package/dist/chainResolver.test.d.ts.map +1 -0
  26. package/dist/chainResolver.test.js +201 -0
  27. package/dist/chainResolver.test.js.map +1 -0
  28. package/dist/execute.d.ts +4 -1
  29. package/dist/execute.d.ts.map +1 -1
  30. package/dist/execute.js +3 -0
  31. package/dist/execute.js.map +1 -1
  32. package/dist/index.js +478 -3
  33. package/dist/index.js.map +1 -1
  34. package/docs/SUBAGENT-NAMING.md +94 -0
  35. package/landing/public/logos/chattgpt.svg +1 -0
  36. package/landing/public/logos/claude.svg +1 -0
  37. package/landing/public/logos/gemini.svg +1 -0
  38. package/landing/public/logos/grok.svg +1 -0
  39. package/landing/src/app/page.tsx +12 -21
  40. package/landing/src/app/workspace/chains/page.tsx +520 -0
  41. package/landing/src/app/workspace/page.tsx +1903 -224
  42. package/landing/src/components/AITestimonials.tsx +15 -9
  43. package/landing/src/components/ChainStepDetail.tsx +310 -0
  44. package/landing/src/components/ChainTrace.tsx +261 -0
  45. package/landing/src/lib/stats.json +1 -1
  46. package/package.json +14 -2
  47. package/src/chainExecutor.ts +730 -0
  48. package/src/chainResolver.test.ts +246 -0
  49. package/src/chainResolver.ts +658 -0
  50. package/src/execute.ts +23 -0
  51. package/src/index.ts +524 -3
@@ -56,6 +56,9 @@ import {
56
56
  Play,
57
57
  Star,
58
58
  Twitter,
59
+ ClipboardList,
60
+ Bot,
61
+ Link as LinkIcon,
59
62
  } from "lucide-react";
60
63
  import {
61
64
  LineChart,
@@ -139,7 +142,7 @@ interface ProviderAnalytics {
139
142
  }
140
143
 
141
144
  type TabType = "overview" | "api-catalog" | "my-agents" | "my-apis" | "analytics" | "webhooks" | "api-keys" | "earn" | "docs" | "feedback" | "settings" | "billing";
142
- type AnalyticsSubtab = "overview" | "usage" | "logs";
145
+ type AnalyticsSubtab = "overview" | "usage" | "logs" | "chains";
143
146
 
144
147
  // Generate preview analytics data for demo
145
148
  function generatePreviewAnalytics(): ProviderAnalytics {
@@ -241,7 +244,7 @@ export default function WorkspacePage() {
241
244
  setActiveTab(tabFromUrl);
242
245
  if (tabFromUrl === "analytics") {
243
246
  setAnalyticsExpanded(true);
244
- if (subFromUrl && ["overview", "usage", "logs"].includes(subFromUrl)) {
247
+ if (subFromUrl && ["overview", "usage", "logs", "chains"].includes(subFromUrl)) {
245
248
  setAnalyticsSubtab(subFromUrl);
246
249
  }
247
250
  }
@@ -552,6 +555,7 @@ export default function WorkspacePage() {
552
555
  overview: "Agent Analytics",
553
556
  usage: "API Analytics",
554
557
  logs: "Logs",
558
+ chains: "Chain Traces",
555
559
  };
556
560
  return subLabels[analyticsSubtab] || "Analytics";
557
561
  }
@@ -1412,6 +1416,28 @@ function MyAPIsTab({ apis }: { apis: ProviderAPI[] }) {
1412
1416
  // AGENTS TAB - Agent-first hierarchy view
1413
1417
  // ============================================
1414
1418
 
1419
+ interface MainAgentData {
1420
+ workspaceId: string;
1421
+ email: string;
1422
+ mainAgentId: string | null;
1423
+ mainAgentName: string | null;
1424
+ aiBackend?: string | null;
1425
+ usageCount: number;
1426
+ createdAt: number;
1427
+ }
1428
+
1429
+ interface SubagentData {
1430
+ id: string;
1431
+ subagentId: string;
1432
+ name: string;
1433
+ description?: string;
1434
+ aiBackend?: string;
1435
+ isRegistered?: boolean;
1436
+ callCount: number;
1437
+ firstSeenAt: number;
1438
+ lastActiveAt: number;
1439
+ }
1440
+
1415
1441
  function AgentsTab({
1416
1442
  agents,
1417
1443
  onRevoke,
@@ -1429,6 +1455,73 @@ function AgentsTab({
1429
1455
  const [editingAgent, setEditingAgent] = useState<string | null>(null);
1430
1456
  const [editName, setEditName] = useState("");
1431
1457
  const [copied, setCopied] = useState(false);
1458
+
1459
+ // Main agent data from backend
1460
+ const [mainAgent, setMainAgent] = useState<MainAgentData | null>(null);
1461
+ const [subagents, setSubagents] = useState<SubagentData[]>([]);
1462
+ const [isLoadingAgents, setIsLoadingAgents] = useState(true);
1463
+
1464
+ // Modal states
1465
+ const [showRegisterModal, setShowRegisterModal] = useState(false);
1466
+ const [editingSubagent, setEditingSubagent] = useState<SubagentData | null>(null);
1467
+ const [expandedSubagent, setExpandedSubagent] = useState<string | null>(null);
1468
+
1469
+ // Register form state
1470
+ const [registerForm, setRegisterForm] = useState({
1471
+ subagentId: "",
1472
+ name: "",
1473
+ description: "",
1474
+ });
1475
+ const [registerLoading, setRegisterLoading] = useState(false);
1476
+ const [registerError, setRegisterError] = useState<string | null>(null);
1477
+
1478
+ // Fetch main agent and subagents data
1479
+ useEffect(() => {
1480
+ const fetchAgentData = async () => {
1481
+ if (!sessionToken) {
1482
+ setIsLoadingAgents(false);
1483
+ return;
1484
+ }
1485
+
1486
+ try {
1487
+ // Fetch main agent
1488
+ const mainRes = await fetch(`${CONVEX_URL}/api/query`, {
1489
+ method: "POST",
1490
+ headers: { "Content-Type": "application/json" },
1491
+ body: JSON.stringify({
1492
+ path: "agents:getMainAgent",
1493
+ args: { token: sessionToken },
1494
+ }),
1495
+ });
1496
+ const mainData = await mainRes.json();
1497
+ const mainResult = mainData.value || mainData;
1498
+ if (mainResult && !mainResult.error) {
1499
+ setMainAgent(mainResult);
1500
+ }
1501
+
1502
+ // Fetch subagents
1503
+ const subRes = await fetch(`${CONVEX_URL}/api/query`, {
1504
+ method: "POST",
1505
+ headers: { "Content-Type": "application/json" },
1506
+ body: JSON.stringify({
1507
+ path: "agents:getSubagents",
1508
+ args: { token: sessionToken, limit: 50 },
1509
+ }),
1510
+ });
1511
+ const subData = await subRes.json();
1512
+ const subResult = subData.value || subData;
1513
+ if (subResult && Array.isArray(subResult.subagents)) {
1514
+ setSubagents(subResult.subagents);
1515
+ }
1516
+ } catch (err) {
1517
+ console.error("Error fetching agent data:", err);
1518
+ } finally {
1519
+ setIsLoadingAgents(false);
1520
+ }
1521
+ };
1522
+
1523
+ fetchAgentData();
1524
+ }, [sessionToken]);
1432
1525
 
1433
1526
  // Get the primary agent (current session or first agent)
1434
1527
  const primaryAgent = agents.find(a => a.isCurrent) || agents[0];
@@ -1462,14 +1555,134 @@ function AgentsTab({
1462
1555
  setTimeout(() => setCopied(false), 2000);
1463
1556
  };
1464
1557
 
1558
+ // Handle register new agent
1559
+ const handleRegisterAgent = async () => {
1560
+ if (!sessionToken || !registerForm.subagentId.trim()) {
1561
+ setRegisterError("Subagent ID is required");
1562
+ return;
1563
+ }
1564
+
1565
+ setRegisterLoading(true);
1566
+ setRegisterError(null);
1567
+
1568
+ try {
1569
+ const res = await fetch(`${CONVEX_URL}/api/mutation`, {
1570
+ method: "POST",
1571
+ headers: { "Content-Type": "application/json" },
1572
+ body: JSON.stringify({
1573
+ path: "agents:registerTaskAgent",
1574
+ args: {
1575
+ token: sessionToken,
1576
+ subagentId: registerForm.subagentId.trim(),
1577
+ name: registerForm.name.trim() || undefined,
1578
+ description: registerForm.description.trim() || undefined,
1579
+ },
1580
+ }),
1581
+ });
1582
+
1583
+ const data = await res.json();
1584
+ if (data.error) {
1585
+ setRegisterError(data.error);
1586
+ return;
1587
+ }
1588
+
1589
+ // Add to subagents list
1590
+ const newSubagent: SubagentData = {
1591
+ id: data.value?.id || data.id,
1592
+ subagentId: registerForm.subagentId.trim(),
1593
+ name: registerForm.name.trim() || registerForm.subagentId.trim(),
1594
+ description: registerForm.description.trim() || undefined,
1595
+ isRegistered: true,
1596
+ callCount: 0,
1597
+ firstSeenAt: Date.now(),
1598
+ lastActiveAt: Date.now(),
1599
+ };
1600
+
1601
+ setSubagents(prev => [newSubagent, ...prev]);
1602
+ setShowRegisterModal(false);
1603
+ setRegisterForm({ subagentId: "", name: "", description: "" });
1604
+ } catch (err) {
1605
+ console.error("Error registering agent:", err);
1606
+ setRegisterError("Failed to register agent");
1607
+ } finally {
1608
+ setRegisterLoading(false);
1609
+ }
1610
+ };
1611
+
1612
+ // Handle update subagent
1613
+ const handleUpdateSubagent = async (subagentId: string, name: string, description?: string) => {
1614
+ if (!sessionToken) return;
1615
+
1616
+ try {
1617
+ await fetch(`${CONVEX_URL}/api/mutation`, {
1618
+ method: "POST",
1619
+ headers: { "Content-Type": "application/json" },
1620
+ body: JSON.stringify({
1621
+ path: "agents:renameSubagent",
1622
+ args: {
1623
+ token: sessionToken,
1624
+ subagentId,
1625
+ name: name.trim(),
1626
+ },
1627
+ }),
1628
+ });
1629
+
1630
+ // Update local state
1631
+ setSubagents(prev => prev.map(s =>
1632
+ s.subagentId === subagentId
1633
+ ? { ...s, name: name.trim(), description: description?.trim() }
1634
+ : s
1635
+ ));
1636
+ setEditingSubagent(null);
1637
+ } catch (err) {
1638
+ console.error("Error updating subagent:", err);
1639
+ }
1640
+ };
1641
+
1642
+ // Handle rename main agent
1643
+ const handleRenameMainAgent = async (name: string) => {
1644
+ if (!sessionToken) return;
1645
+
1646
+ try {
1647
+ await fetch(`${CONVEX_URL}/api/mutation`, {
1648
+ method: "POST",
1649
+ headers: { "Content-Type": "application/json" },
1650
+ body: JSON.stringify({
1651
+ path: "agents:renameMainAgent",
1652
+ args: { token: sessionToken, name: name.trim() },
1653
+ }),
1654
+ });
1655
+
1656
+ setMainAgent(prev => prev ? { ...prev, mainAgentName: name.trim() } : prev);
1657
+ } catch (err) {
1658
+ console.error("Error renaming main agent:", err);
1659
+ }
1660
+ };
1661
+
1662
+ // Format relative time
1663
+ const formatRelativeTime = (timestamp: number) => {
1664
+ const now = Date.now();
1665
+ const diff = now - timestamp;
1666
+
1667
+ if (diff < 60000) return "Just now";
1668
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
1669
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)} hours ago`;
1670
+ if (diff < 604800000) return `${Math.floor(diff / 86400000)} days ago`;
1671
+
1672
+ return new Date(timestamp).toLocaleDateString();
1673
+ };
1674
+
1465
1675
  const mcpCommand = "npx @nordsym/apiclaw mcp-install";
1466
1676
 
1467
1677
  return (
1468
1678
  <div className="space-y-6">
1469
- {/* Your Agent - Primary Card */}
1679
+ {/* Primary Agent Card */}
1470
1680
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1471
1681
  <div className="flex items-center justify-between mb-4">
1472
- <span className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">Your Agent</span>
1682
+ <div className="flex items-center gap-2">
1683
+ <Bot className="w-6 h-6 text-[#ef4444]" />
1684
+ <span className="text-sm font-medium text-[var(--text-muted)] uppercase tracking-wider">Primary Agent</span>
1685
+ </div>
1473
1686
  {primaryAgent && (
1474
1687
  <button
1475
1688
  onClick={() => handleRevoke(primaryAgent.id)}
@@ -1485,71 +1698,109 @@ function AgentsTab({
1485
1698
  )}
1486
1699
  </div>
1487
1700
 
1488
- {primaryAgent ? (
1489
- <div className="flex items-center gap-4">
1490
- <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-[#ef4444] to-[#f97316] flex items-center justify-center flex-shrink-0">
1491
- <Cpu className="w-7 h-7 text-white" />
1492
- </div>
1493
- <div className="flex-1 min-w-0">
1494
- {editingAgent === primaryAgent.id ? (
1495
- <div className="flex items-center gap-2">
1496
- <input
1497
- type="text"
1498
- value={editName}
1499
- onChange={(e) => setEditName(e.target.value)}
1500
- placeholder="Agent name..."
1501
- className="flex-1 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1502
- autoFocus
1503
- onKeyDown={(e) => {
1504
- if (e.key === "Enter") {
1505
- onRename(primaryAgent.id, editName);
1506
- setEditingAgent(null);
1507
- } else if (e.key === "Escape") {
1508
- setEditingAgent(null);
1509
- }
1510
- }}
1511
- />
1512
- <button
1513
- onClick={() => {
1514
- onRename(primaryAgent.id, editName);
1515
- setEditingAgent(null);
1516
- }}
1517
- className="px-3 py-1.5 bg-[#ef4444] text-white rounded-lg text-sm hover:bg-[#dc2626]"
1518
- >
1519
- Save
1520
- </button>
1521
- <button
1522
- onClick={() => setEditingAgent(null)}
1523
- className="px-3 py-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1524
- >
1525
- Cancel
1526
- </button>
1701
+ {isLoadingAgents ? (
1702
+ <div className="flex items-center justify-center py-8">
1703
+ <Loader2 className="w-6 h-6 text-[#ef4444] animate-spin" />
1704
+ </div>
1705
+ ) : primaryAgent ? (
1706
+ <div className="space-y-4">
1707
+ {/* Agent name with edit */}
1708
+ <div className="flex items-start justify-between">
1709
+ <div className="flex items-center gap-3">
1710
+ <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-[#ef4444] to-[#f97316] flex items-center justify-center flex-shrink-0">
1711
+ <Cpu className="w-7 h-7 text-white" />
1527
1712
  </div>
1528
- ) : (
1529
- <div className="flex items-center gap-2">
1530
- <h3 className="text-xl font-bold">{getAgentDisplayName(primaryAgent)}</h3>
1531
- <button
1532
- onClick={() => {
1533
- setEditingAgent(primaryAgent.id);
1534
- setEditName(primaryAgent.name || getAgentDisplayName(primaryAgent));
1535
- }}
1536
- className="p-1 rounded text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--surface)] transition"
1537
- title="Rename agent"
1538
- >
1539
- <Settings className="w-4 h-4" />
1540
- </button>
1713
+ <div>
1714
+ {editingAgent === "main" ? (
1715
+ <div className="flex items-center gap-2">
1716
+ <input
1717
+ type="text"
1718
+ value={editName}
1719
+ onChange={(e) => setEditName(e.target.value)}
1720
+ placeholder="Agent name..."
1721
+ className="px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1722
+ autoFocus
1723
+ onKeyDown={(e) => {
1724
+ if (e.key === "Enter") {
1725
+ handleRenameMainAgent(editName);
1726
+ onRename(primaryAgent.id, editName);
1727
+ setEditingAgent(null);
1728
+ } else if (e.key === "Escape") {
1729
+ setEditingAgent(null);
1730
+ }
1731
+ }}
1732
+ />
1733
+ <button
1734
+ onClick={() => {
1735
+ handleRenameMainAgent(editName);
1736
+ onRename(primaryAgent.id, editName);
1737
+ setEditingAgent(null);
1738
+ }}
1739
+ className="px-3 py-1.5 bg-[#ef4444] text-white rounded-lg text-sm hover:bg-[#dc2626]"
1740
+ >
1741
+ Save
1742
+ </button>
1743
+ <button
1744
+ onClick={() => setEditingAgent(null)}
1745
+ className="px-3 py-1.5 text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1746
+ >
1747
+ Cancel
1748
+ </button>
1749
+ </div>
1750
+ ) : (
1751
+ <div className="flex items-center gap-2">
1752
+ <h3 className="text-xl font-bold">
1753
+ {mainAgent?.mainAgentName || getAgentDisplayName(primaryAgent)}
1754
+ </h3>
1755
+ <button
1756
+ onClick={() => {
1757
+ setEditingAgent("main");
1758
+ setEditName(mainAgent?.mainAgentName || getAgentDisplayName(primaryAgent));
1759
+ }}
1760
+ className="px-2 py-1 rounded text-xs text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--surface)] transition"
1761
+ >
1762
+ Edit
1763
+ </button>
1764
+ </div>
1765
+ )}
1766
+ <div className="flex items-center gap-2 mt-1">
1767
+ <span className="flex items-center gap-1.5 text-sm text-[var(--text-muted)]">
1768
+ <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
1769
+ Connected
1770
+ </span>
1771
+ </div>
1541
1772
  </div>
1542
- )}
1543
- <div className="flex items-center gap-3 mt-1 text-sm text-[var(--text-muted)]">
1544
- <span className="flex items-center gap-1.5">
1545
- <span className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
1546
- Connected
1547
- </span>
1548
- <span className="text-[var(--border)]">•</span>
1549
- <span className="flex items-center gap-1">
1550
- <Activity className="w-3.5 h-3.5" />
1551
- Active {new Date(primaryAgent.lastUsedAt).toLocaleDateString()}
1552
- </span>
1773
+ </div>
1774
+ </div>
1775
+
1776
+ {/* Agent details grid */}
1777
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-4 border-t border-[var(--border)]">
1778
+ <div>
1779
+ <p className="text-xs text-[var(--text-muted)] mb-1">Agent ID</p>
1780
+ <p className="font-mono text-sm truncate" title={mainAgent?.mainAgentId || primaryAgent.fingerprint}>
1781
+ {(mainAgent?.mainAgentId || primaryAgent.fingerprint)?.slice(0, 12)}...
1782
+ </p>
1783
+ </div>
1784
+ <div>
1785
+ <p className="text-xs text-[var(--text-muted)] mb-1">AI Backend</p>
1786
+ <p className="text-sm">
1787
+ {mainAgent?.aiBackend ? (
1788
+ <span className="flex items-center gap-1.5">
1789
+ <Sparkles className="w-3.5 h-3.5 text-[#ef4444]" />
1790
+ {mainAgent.aiBackend}
1791
+ </span>
1792
+ ) : (
1793
+ <span className="text-[var(--text-muted)]">Not detected</span>
1794
+ )}
1795
+ </p>
1796
+ </div>
1797
+ <div>
1798
+ <p className="text-xs text-[var(--text-muted)] mb-1">Total Calls</p>
1799
+ <p className="text-sm font-semibold">{(mainAgent?.usageCount || 0).toLocaleString()}</p>
1800
+ </div>
1801
+ <div>
1802
+ <p className="text-xs text-[var(--text-muted)] mb-1">Last Active</p>
1803
+ <p className="text-sm">{formatRelativeTime(primaryAgent.lastUsedAt)}</p>
1553
1804
  </div>
1554
1805
  </div>
1555
1806
  </div>
@@ -1569,22 +1820,85 @@ function AgentsTab({
1569
1820
  {/* Subagents Section */}
1570
1821
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1571
1822
  <div className="flex items-center justify-between mb-4">
1572
- <span className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider">Subagents</span>
1823
+ <div className="flex items-center gap-2">
1824
+ <ClipboardList className="w-5 h-5 text-[#ef4444]" />
1825
+ <span className="text-sm font-medium text-[var(--text-muted)] uppercase tracking-wider">
1826
+ SUBAGENTS ({subagents.length})
1827
+ </span>
1828
+ </div>
1573
1829
  </div>
1574
1830
 
1575
- {/* Empty state - subagents will come from backend later */}
1576
- <div className="py-8 text-center">
1577
- <div className="w-12 h-12 rounded-xl bg-[var(--surface)] mx-auto mb-3 flex items-center justify-center">
1578
- <Users className="w-6 h-6 text-[var(--text-muted)]" />
1831
+ {isLoadingAgents ? (
1832
+ <div className="flex items-center justify-center py-8">
1833
+ <Loader2 className="w-6 h-6 text-[#ef4444] animate-spin" />
1579
1834
  </div>
1580
- <p className="text-sm text-[var(--text-muted)] max-w-sm mx-auto">
1581
- Subagents will appear here when your agent makes calls with the{" "}
1582
- <code className="px-1.5 py-0.5 rounded bg-[var(--surface)] text-[#ef4444] font-mono text-xs">
1583
- X-APIClaw-Subagent
1584
- </code>{" "}
1585
- header.
1586
- </p>
1587
- </div>
1835
+ ) : subagents.length > 0 ? (
1836
+ <div className="space-y-3">
1837
+ {subagents.map((subagent) => (
1838
+ <div
1839
+ key={subagent.id}
1840
+ className="p-4 rounded-xl bg-[var(--surface)] border border-[var(--border)] cursor-pointer hover:bg-white/5 transition-colors"
1841
+ onClick={() => setExpandedSubagent(
1842
+ expandedSubagent === subagent.subagentId ? null : subagent.subagentId
1843
+ )}
1844
+ >
1845
+ <div className="flex items-center justify-between">
1846
+ <div className="flex items-center gap-3">
1847
+ <div className="w-10 h-10 rounded-lg bg-[var(--background)] flex items-center justify-center flex-shrink-0">
1848
+ <Users className="w-5 h-5 text-[var(--text-muted)]" />
1849
+ </div>
1850
+ <div>
1851
+ <div className="flex items-center gap-2">
1852
+ <p className="font-medium">{subagent.name || subagent.subagentId}</p>
1853
+ {subagent.isRegistered && (
1854
+ <span className="px-1.5 py-0.5 rounded text-[10px] bg-[#ef4444]/10 text-[#ef4444] font-medium">
1855
+ Registered
1856
+ </span>
1857
+ )}
1858
+ </div>
1859
+ <p className="text-sm text-[var(--text-muted)]">
1860
+ Calls: {subagent.callCount.toLocaleString()} • Last: {formatRelativeTime(subagent.lastActiveAt)}
1861
+ </p>
1862
+ {subagent.aiBackend && (
1863
+ <p className="text-sm text-[var(--text-muted)]">
1864
+ AI Backend: {subagent.aiBackend}
1865
+ </p>
1866
+ )}
1867
+ </div>
1868
+ </div>
1869
+ <ChevronDown
1870
+ className={`w-5 h-5 text-[var(--text-muted)] transition-transform duration-200 ${
1871
+ expandedSubagent === subagent.subagentId ? 'rotate-180' : ''
1872
+ }`}
1873
+ />
1874
+ </div>
1875
+
1876
+ {/* Expanded content */}
1877
+ {expandedSubagent === subagent.subagentId && (
1878
+ <div className="mt-4 pt-4 border-t border-[var(--border)]" onClick={(e) => e.stopPropagation()}>
1879
+ <SubagentActivityLog
1880
+ token={sessionToken || ''}
1881
+ subagentId={subagent.subagentId}
1882
+ />
1883
+ </div>
1884
+ )}
1885
+ </div>
1886
+ ))}
1887
+ </div>
1888
+ ) : (
1889
+ <div className="py-8 text-center">
1890
+ <div className="w-12 h-12 rounded-xl bg-[var(--surface)] mx-auto mb-3 flex items-center justify-center">
1891
+ <Users className="w-6 h-6 text-[var(--text-muted)]" />
1892
+ </div>
1893
+ <p className="text-sm text-[var(--text-muted)] max-w-sm mx-auto">
1894
+ Subagents appear here when your agent makes calls with the{" "}
1895
+ <code className="px-1.5 py-0.5 rounded bg-[var(--background)] text-[#ef4444] font-mono text-xs">
1896
+ X-APIClaw-Subagent
1897
+ </code>{" "}
1898
+ header.
1899
+ </p>
1900
+ </div>
1901
+ )}
1588
1902
  </div>
1589
1903
 
1590
1904
  {/* Quick Setup - Collapsed at bottom */}
@@ -1616,124 +1930,1163 @@ function AgentsTab({
1616
1930
  </code>
1617
1931
  </p>
1618
1932
  </div>
1619
- </div>
1620
- );
1621
- }
1622
1933
 
1623
- // ============================================
1624
- // ANALYTICS TAB (with subtabs)
1625
- // ============================================
1934
+ {/* Register New Agent Modal */}
1935
+ {showRegisterModal && (
1936
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
1937
+ <div className="bg-[var(--surface-elevated)] rounded-2xl border border-[var(--border)] w-full max-w-md">
1938
+ <div className="flex items-center justify-between p-6 border-b border-[var(--border)]">
1939
+ <h3 className="text-lg font-bold">Register New Agent</h3>
1940
+ <button
1941
+ onClick={() => {
1942
+ setShowRegisterModal(false);
1943
+ setRegisterForm({ subagentId: "", name: "", description: "" });
1944
+ setRegisterError(null);
1945
+ }}
1946
+ className="p-1 rounded hover:bg-[var(--surface)] transition"
1947
+ >
1948
+ <X className="w-5 h-5" />
1949
+ </button>
1950
+ </div>
1951
+
1952
+ <div className="p-6 space-y-4">
1953
+ <div>
1954
+ <label className="block text-sm font-medium mb-1.5">
1955
+ Subagent ID <span className="text-red-500">*</span>
1956
+ </label>
1957
+ <input
1958
+ type="text"
1959
+ value={registerForm.subagentId}
1960
+ onChange={(e) => setRegisterForm(f => ({ ...f, subagentId: e.target.value }))}
1961
+ placeholder="research-agent"
1962
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1963
+ />
1964
+ <p className="text-xs text-[var(--text-muted)] mt-1">
1965
+ This will be sent in the X-APIClaw-Subagent header
1966
+ </p>
1967
+ </div>
1968
+
1969
+ <div>
1970
+ <label className="block text-sm font-medium mb-1.5">Display Name</label>
1971
+ <input
1972
+ type="text"
1973
+ value={registerForm.name}
1974
+ onChange={(e) => setRegisterForm(f => ({ ...f, name: e.target.value }))}
1975
+ placeholder="Research Agent"
1976
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1977
+ />
1978
+ </div>
1979
+
1980
+ <div>
1981
+ <label className="block text-sm font-medium mb-1.5">Description</label>
1982
+ <textarea
1983
+ value={registerForm.description}
1984
+ onChange={(e) => setRegisterForm(f => ({ ...f, description: e.target.value }))}
1985
+ placeholder="Researches topics and competitors"
1986
+ rows={2}
1987
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50 resize-none"
1988
+ />
1989
+ </div>
1626
1990
 
1627
- function StatCard({
1628
- title,
1629
- value,
1630
- change,
1631
- icon: Icon,
1632
- accent,
1633
- }: {
1634
- title: string;
1635
- value: string;
1636
- change?: number;
1637
- icon: typeof Zap;
1638
- accent?: boolean;
1639
- }) {
1640
- return (
1641
- <div className={`rounded-xl sm:rounded-2xl border p-3 sm:p-5 ${accent ? "bg-[#ef4444]/10 border-[#ef4444]/30" : "bg-[var(--surface-elevated)] border-[var(--border)]"}`}>
1642
- <div className="flex items-center justify-between mb-2 sm:mb-3">
1643
- <span className="text-xs sm:text-sm text-[var(--text-muted)] truncate pr-2">{title}</span>
1644
- <Icon className={`w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0 ${accent ? "text-[#ef4444]" : "text-[var(--text-muted)]"}`} />
1645
- </div>
1646
- <div className="flex items-end justify-between">
1647
- <span className={`text-xl sm:text-3xl font-bold ${accent ? "text-[#ef4444]" : ""}`}>{value}</span>
1648
- {change !== undefined && (
1649
- <div className={`flex items-center gap-1 text-xs sm:text-sm ${change >= 0 ? "text-green-500" : "text-red-500"}`}>
1650
- {change >= 0 ? <ArrowUpRight className="w-3 h-3 sm:w-4 sm:h-4" /> : <ArrowDownRight className="w-3 h-3 sm:w-4 sm:h-4" />}
1651
- {Math.abs(change).toFixed(1)}%
1991
+ {registerError && (
1992
+ <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 text-sm">
1993
+ {registerError}
1994
+ </div>
1995
+ )}
1996
+ </div>
1997
+
1998
+ <div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--border)]">
1999
+ <button
2000
+ onClick={() => {
2001
+ setShowRegisterModal(false);
2002
+ setRegisterForm({ subagentId: "", name: "", description: "" });
2003
+ setRegisterError(null);
2004
+ }}
2005
+ className="px-4 py-2 rounded-lg text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] transition"
2006
+ >
2007
+ Cancel
2008
+ </button>
2009
+ <button
2010
+ onClick={handleRegisterAgent}
2011
+ disabled={registerLoading || !registerForm.subagentId.trim()}
2012
+ className="px-4 py-2 rounded-lg text-sm font-medium bg-[#ef4444] text-white hover:bg-[#dc2626] transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
2013
+ >
2014
+ {registerLoading && <Loader2 className="w-4 h-4 animate-spin" />}
2015
+ Register Agent
2016
+ </button>
2017
+ </div>
1652
2018
  </div>
1653
- )}
1654
- </div>
2019
+ </div>
2020
+ )}
2021
+
2022
+ {/* Edit Subagent Modal */}
2023
+ {editingSubagent && (
2024
+ <EditSubagentModal
2025
+ subagent={editingSubagent}
2026
+ onClose={() => setEditingSubagent(null)}
2027
+ onSave={handleUpdateSubagent}
2028
+ />
2029
+ )}
1655
2030
  </div>
1656
2031
  );
1657
2032
  }
1658
2033
 
1659
- function AnalyticsTab({
1660
- apis,
1661
- analytics,
1662
- workspace,
1663
- agents,
1664
- usage,
1665
- activeSubtab,
1666
- setActiveSubtab,
1667
- sessionToken,
1668
- }: {
1669
- apis: ProviderAPI[];
1670
- analytics: ProviderAnalytics | null;
1671
- workspace: Workspace | null;
1672
- agents: Agent[];
1673
- usage: UsageData | null;
1674
- activeSubtab: AnalyticsSubtab;
1675
- setActiveSubtab: (tab: AnalyticsSubtab) => void;
1676
- sessionToken: string | null;
1677
- }) {
1678
- const router = useRouter();
2034
+ // Subagent Activity Log Component
2035
+ const SubagentActivityLog = ({ token, subagentId }: { token: string; subagentId: string }) => {
2036
+ const [activity, setActivity] = useState<any[]>([]);
2037
+ const [loading, setLoading] = useState(true);
2038
+
2039
+ useEffect(() => {
2040
+ const fetchActivity = async () => {
2041
+ try {
2042
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
2043
+ method: 'POST',
2044
+ headers: { 'Content-Type': 'application/json' },
2045
+ body: JSON.stringify({
2046
+ path: 'logs:getBySubagent',
2047
+ args: { token, subagentId, limit: 10 }
2048
+ }),
2049
+ });
2050
+ const data = await res.json();
2051
+ if (data.value) {
2052
+ setActivity(data.value);
2053
+ }
2054
+ } catch (e) {
2055
+ console.error('Failed to fetch subagent activity', e);
2056
+ } finally {
2057
+ setLoading(false);
2058
+ }
2059
+ };
2060
+ fetchActivity();
2061
+ }, [token, subagentId]);
2062
+
2063
+ if (loading) {
2064
+ return (
2065
+ <div className="flex items-center gap-2 text-sm text-[var(--text-muted)]">
2066
+ <Loader2 className="w-4 h-4 animate-spin" />
2067
+ Loading activity...
2068
+ </div>
2069
+ );
2070
+ }
2071
+
2072
+ if (activity.length === 0) {
2073
+ return <p className="text-sm text-[var(--text-muted)]">No activity yet</p>;
2074
+ }
2075
+
2076
+ // Local TypeBadge for activity log
2077
+ const ActivityTypeBadge = ({ type }: { type: string }) => {
2078
+ const badges: Record<string, { bg: string; text: string; label: string }> = {
2079
+ search: { bg: 'bg-blue-500/20', text: 'text-blue-400', label: 'Search' },
2080
+ call: { bg: 'bg-green-500/20', text: 'text-green-400', label: 'Call' },
2081
+ direct_call: { bg: 'bg-[#ef4444]/20', text: 'text-[#ef4444]', label: 'Direct' },
2082
+ error: { bg: 'bg-red-500/20', text: 'text-red-400', label: 'Error' },
2083
+ };
2084
+ const badge = badges[type] || { bg: 'bg-gray-500/20', text: 'text-gray-400', label: type };
2085
+ return (
2086
+ <span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${badge.bg} ${badge.text}`}>
2087
+ {badge.label}
2088
+ </span>
2089
+ );
2090
+ };
2091
+
2092
+ return (
2093
+ <div className="space-y-2">
2094
+ <p className="text-xs font-medium text-[var(--text-muted)] uppercase">Recent Activity</p>
2095
+ <div className="space-y-1">
2096
+ {activity.map((item, i) => (
2097
+ <div key={i} className="flex items-center justify-between text-sm py-1">
2098
+ <div className="flex items-center gap-2">
2099
+ <ActivityTypeBadge type={item.type} />
2100
+ <span className="text-[var(--text-secondary)]">
2101
+ {item.type === 'search' ? item.query : `${item.provider || 'API'}.${item.action || 'call'}`}
2102
+ </span>
2103
+ </div>
2104
+ <span className="text-[var(--text-muted)]">
2105
+ {item.latencyMs || item.responseTimeMs || '-'}ms
2106
+ </span>
2107
+ </div>
2108
+ ))}
2109
+ </div>
2110
+ </div>
2111
+ );
2112
+ };
2113
+
2114
+ // Edit Subagent Modal Component
2115
+ function EditSubagentModal({
2116
+ subagent,
2117
+ onClose,
2118
+ onSave,
2119
+ }: {
2120
+ subagent: SubagentData;
2121
+ onClose: () => void;
2122
+ onSave: (subagentId: string, name: string, description?: string) => void;
2123
+ }) {
2124
+ const [name, setName] = useState(subagent.name);
2125
+ const [description, setDescription] = useState(subagent.description || "");
2126
+ const [isSaving, setIsSaving] = useState(false);
2127
+
2128
+ const handleSave = async () => {
2129
+ setIsSaving(true);
2130
+ await onSave(subagent.subagentId, name, description);
2131
+ setIsSaving(false);
2132
+ };
2133
+
2134
+ return (
2135
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
2136
+ <div className="bg-[var(--surface-elevated)] rounded-2xl border border-[var(--border)] w-full max-w-md">
2137
+ <div className="flex items-center justify-between p-6 border-b border-[var(--border)]">
2138
+ <h3 className="text-lg font-bold">Edit Agent</h3>
2139
+ <button onClick={onClose} className="p-1 rounded hover:bg-[var(--surface)] transition">
2140
+ <X className="w-5 h-5" />
2141
+ </button>
2142
+ </div>
2143
+
2144
+ <div className="p-6 space-y-4">
2145
+ <div>
2146
+ <label className="block text-sm font-medium mb-1.5">Subagent ID</label>
2147
+ <input
2148
+ type="text"
2149
+ value={subagent.subagentId}
2150
+ disabled
2151
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--surface)] text-sm text-[var(--text-muted)] cursor-not-allowed"
2152
+ />
2153
+ <p className="text-xs text-[var(--text-muted)] mt-1">ID cannot be changed</p>
2154
+ </div>
2155
+
2156
+ <div>
2157
+ <label className="block text-sm font-medium mb-1.5">Display Name</label>
2158
+ <input
2159
+ type="text"
2160
+ value={name}
2161
+ onChange={(e) => setName(e.target.value)}
2162
+ placeholder="Agent name"
2163
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
2164
+ />
2165
+ </div>
2166
+
2167
+ <div>
2168
+ <label className="block text-sm font-medium mb-1.5">Description</label>
2169
+ <textarea
2170
+ value={description}
2171
+ onChange={(e) => setDescription(e.target.value)}
2172
+ placeholder="What does this agent do?"
2173
+ rows={2}
2174
+ className="w-full px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50 resize-none"
2175
+ />
2176
+ </div>
2177
+
2178
+ {subagent.aiBackend && (
2179
+ <div>
2180
+ <label className="block text-sm font-medium mb-1.5">AI Backend</label>
2181
+ <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-[var(--surface)] text-sm">
2182
+ <Sparkles className="w-4 h-4 text-[#ef4444]" />
2183
+ {subagent.aiBackend}
2184
+ </div>
2185
+ <p className="text-xs text-[var(--text-muted)] mt-1">Auto-detected from API calls</p>
2186
+ </div>
2187
+ )}
2188
+ </div>
2189
+
2190
+ <div className="flex items-center justify-end gap-3 p-6 border-t border-[var(--border)]">
2191
+ <button
2192
+ onClick={onClose}
2193
+ className="px-4 py-2 rounded-lg text-sm font-medium text-[var(--text-muted)] hover:text-[var(--text-primary)] transition"
2194
+ >
2195
+ Cancel
2196
+ </button>
2197
+ <button
2198
+ onClick={handleSave}
2199
+ disabled={isSaving || !name.trim()}
2200
+ className="px-4 py-2 rounded-lg text-sm font-medium bg-[#ef4444] text-white hover:bg-[#dc2626] transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
2201
+ >
2202
+ {isSaving && <Loader2 className="w-4 h-4 animate-spin" />}
2203
+ Save Changes
2204
+ </button>
2205
+ </div>
2206
+ </div>
2207
+ </div>
2208
+ );
2209
+ }
2210
+
2211
+ // ============================================
2212
+ // ANALYTICS TAB (with subtabs)
2213
+ // ============================================
2214
+
2215
+ function StatCard({
2216
+ title,
2217
+ value,
2218
+ change,
2219
+ icon: Icon,
2220
+ accent,
2221
+ }: {
2222
+ title: string;
2223
+ value: string;
2224
+ change?: number;
2225
+ icon: typeof Zap;
2226
+ accent?: boolean;
2227
+ }) {
2228
+ return (
2229
+ <div className={`rounded-xl sm:rounded-2xl border p-3 sm:p-5 ${accent ? "bg-[#ef4444]/10 border-[#ef4444]/30" : "bg-[var(--surface-elevated)] border-[var(--border)]"}`}>
2230
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
2231
+ <span className="text-xs sm:text-sm text-[var(--text-muted)] truncate pr-2">{title}</span>
2232
+ <Icon className={`w-4 h-4 sm:w-5 sm:h-5 flex-shrink-0 ${accent ? "text-[#ef4444]" : "text-[var(--text-muted)]"}`} />
2233
+ </div>
2234
+ <div className="flex items-end justify-between">
2235
+ <span className={`text-xl sm:text-3xl font-bold ${accent ? "text-[#ef4444]" : ""}`}>{value}</span>
2236
+ {change !== undefined && (
2237
+ <div className={`flex items-center gap-1 text-xs sm:text-sm ${change >= 0 ? "text-green-500" : "text-red-500"}`}>
2238
+ {change >= 0 ? <ArrowUpRight className="w-3 h-3 sm:w-4 sm:h-4" /> : <ArrowDownRight className="w-3 h-3 sm:w-4 sm:h-4" />}
2239
+ {Math.abs(change).toFixed(1)}%
2240
+ </div>
2241
+ )}
2242
+ </div>
2243
+ </div>
2244
+ );
2245
+ }
2246
+
2247
+ function AnalyticsTab({
2248
+ apis,
2249
+ analytics,
2250
+ workspace,
2251
+ agents,
2252
+ usage,
2253
+ activeSubtab,
2254
+ setActiveSubtab,
2255
+ sessionToken,
2256
+ }: {
2257
+ apis: ProviderAPI[];
2258
+ analytics: ProviderAnalytics | null;
2259
+ workspace: Workspace | null;
2260
+ agents: Agent[];
2261
+ usage: UsageData | null;
2262
+ activeSubtab: AnalyticsSubtab;
2263
+ setActiveSubtab: (tab: AnalyticsSubtab) => void;
2264
+ sessionToken: string | null;
2265
+ }) {
2266
+ const router = useRouter();
2267
+
2268
+ return (
2269
+ <div className="space-y-6">
2270
+ {/* Subtab Navigation */}
2271
+ <div className="flex items-center gap-1 p-1 bg-[var(--surface)] rounded-xl w-fit">
2272
+ <button
2273
+ onClick={() => {
2274
+ setActiveSubtab("overview");
2275
+ router.push("/workspace?tab=analytics&sub=overview");
2276
+ }}
2277
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
2278
+ activeSubtab === "overview"
2279
+ ? "bg-[#ef4444] text-white"
2280
+ : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
2281
+ }`}
2282
+ >
2283
+ <BarChart3 className="w-4 h-4" />
2284
+ Overview
2285
+ </button>
2286
+ <button
2287
+ onClick={() => {
2288
+ setActiveSubtab("usage");
2289
+ router.push("/workspace?tab=analytics&sub=usage");
2290
+ }}
2291
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
2292
+ activeSubtab === "usage"
2293
+ ? "bg-[#ef4444] text-white"
2294
+ : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
2295
+ }`}
2296
+ >
2297
+ <TrendingUp className="w-4 h-4" />
2298
+ Usage
2299
+ </button>
2300
+ <button
2301
+ onClick={() => {
2302
+ setActiveSubtab("logs");
2303
+ router.push("/workspace?tab=analytics&sub=logs");
2304
+ }}
2305
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
2306
+ activeSubtab === "logs"
2307
+ ? "bg-[#ef4444] text-white"
2308
+ : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
2309
+ }`}
2310
+ >
2311
+ <ScrollText className="w-4 h-4" />
2312
+ Logs
2313
+ </button>
2314
+ <button
2315
+ onClick={() => {
2316
+ setActiveSubtab("chains");
2317
+ router.push("/workspace?tab=analytics&sub=chains");
2318
+ }}
2319
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
2320
+ activeSubtab === "chains"
2321
+ ? "bg-[#ef4444] text-white"
2322
+ : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
2323
+ }`}
2324
+ >
2325
+ <Activity className="w-4 h-4" />
2326
+ Chains
2327
+ </button>
2328
+ </div>
2329
+
2330
+ {/* Subtab Content */}
2331
+ {activeSubtab === "overview" && (
2332
+ <AnalyticsOverviewTab apis={apis} analytics={analytics} workspace={workspace} agents={agents} usage={usage} sessionToken={sessionToken} />
2333
+ )}
2334
+ {activeSubtab === "usage" && (
2335
+ <UsageTab workspace={workspace} usage={usage} sessionToken={sessionToken} />
2336
+ )}
2337
+ {activeSubtab === "logs" && (
2338
+ <LogsTab sessionToken={sessionToken} />
2339
+ )}
2340
+ {activeSubtab === "chains" && (
2341
+ <ChainsTab sessionToken={sessionToken} />
2342
+ )}
2343
+ </div>
2344
+ );
2345
+ }
2346
+
2347
+ // ============================================
2348
+ // SEARCH ANALYTICS TAB
2349
+ // ============================================
2350
+
2351
+ interface SearchStats {
2352
+ totalSearches: number;
2353
+ zeroResults: number;
2354
+ zeroResultRate: number;
2355
+ avgResponseTime: number;
2356
+ topQueries: { query: string; count: number }[];
2357
+ topZeroResults: { query: string; count: number }[];
2358
+ bySubagent: Record<string, number>;
2359
+ }
2360
+
2361
+ interface RecentSearch {
2362
+ _id: string;
2363
+ subagentId?: string;
2364
+ query: string;
2365
+ resultCount: number;
2366
+ hasResults: boolean;
2367
+ responseTimeMs: number;
2368
+ timestamp: number;
2369
+ }
2370
+
2371
+ function SearchAnalyticsTab({ sessionToken }: { sessionToken: string | null }) {
2372
+ const [stats, setStats] = useState<SearchStats | null>(null);
2373
+ const [recentSearches, setRecentSearches] = useState<RecentSearch[]>([]);
2374
+ const [isLoading, setIsLoading] = useState(true);
2375
+ const [hoursBack, setHoursBack] = useState(24);
2376
+
2377
+ const fetchSearchData = useCallback(async () => {
2378
+ if (!sessionToken) {
2379
+ setIsLoading(false);
2380
+ return;
2381
+ }
2382
+
2383
+ try {
2384
+ // Fetch stats
2385
+ const statsRes = await fetch(`${CONVEX_URL}/api/query`, {
2386
+ method: "POST",
2387
+ headers: { "Content-Type": "application/json" },
2388
+ body: JSON.stringify({
2389
+ path: "searchLogs:getStats",
2390
+ args: { token: sessionToken, hoursBack },
2391
+ }),
2392
+ });
2393
+ const statsData = await statsRes.json();
2394
+ const statsResult = statsData.value || statsData;
2395
+ if (statsResult && !statsResult.error) {
2396
+ setStats(statsResult);
2397
+ }
2398
+
2399
+ // Fetch recent searches
2400
+ const recentRes = await fetch(`${CONVEX_URL}/api/query`, {
2401
+ method: "POST",
2402
+ headers: { "Content-Type": "application/json" },
2403
+ body: JSON.stringify({
2404
+ path: "searchLogs:getRecent",
2405
+ args: { token: sessionToken, limit: 50 },
2406
+ }),
2407
+ });
2408
+ const recentData = await recentRes.json();
2409
+ const recentResult = recentData.value || recentData;
2410
+ if (Array.isArray(recentResult)) {
2411
+ setRecentSearches(recentResult);
2412
+ }
2413
+ } catch (err) {
2414
+ console.error("Error fetching search analytics:", err);
2415
+ } finally {
2416
+ setIsLoading(false);
2417
+ }
2418
+ }, [sessionToken, hoursBack]);
2419
+
2420
+ useEffect(() => {
2421
+ fetchSearchData();
2422
+ }, [fetchSearchData]);
2423
+
2424
+ const formatTime = (timestamp: number) => {
2425
+ const date = new Date(timestamp);
2426
+ const now = new Date();
2427
+ const diff = now.getTime() - date.getTime();
2428
+
2429
+ if (diff < 60000) return "Just now";
2430
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
2431
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
2432
+
2433
+ return date.toLocaleDateString("en-US", {
2434
+ month: "short",
2435
+ day: "numeric",
2436
+ hour: "2-digit",
2437
+ minute: "2-digit",
2438
+ });
2439
+ };
2440
+
2441
+ if (!sessionToken) {
2442
+ return (
2443
+ <div className="space-y-6">
2444
+ <div className="rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)]/50 p-12 text-center">
2445
+ <Search className="w-16 h-16 text-[var(--text-muted)] mx-auto mb-4" />
2446
+ <h3 className="font-semibold text-xl mb-2">Not Logged In</h3>
2447
+ <p className="text-[var(--text-muted)]">Please log in to view search analytics.</p>
2448
+ </div>
2449
+ </div>
2450
+ );
2451
+ }
2452
+
2453
+ if (isLoading) {
2454
+ return (
2455
+ <div className="flex items-center justify-center py-12">
2456
+ <Loader2 className="w-8 h-8 text-[#ef4444] animate-spin" />
2457
+ </div>
2458
+ );
2459
+ }
2460
+
2461
+ // Check if we have any data
2462
+ const hasData = stats && stats.totalSearches > 0;
2463
+
2464
+ // Preview data for empty state
2465
+ const previewStats: SearchStats = {
2466
+ totalSearches: 247,
2467
+ zeroResults: 18,
2468
+ zeroResultRate: 7.3,
2469
+ avgResponseTime: 89,
2470
+ topQueries: [
2471
+ { query: "send sms", count: 45 },
2472
+ { query: "generate image", count: 38 },
2473
+ { query: "web search", count: 31 },
2474
+ { query: "email api", count: 24 },
2475
+ { query: "transcribe audio", count: 19 },
2476
+ ],
2477
+ topZeroResults: [
2478
+ { query: "blockchain validator", count: 8 },
2479
+ { query: "calendar integration", count: 5 },
2480
+ { query: "video editing", count: 3 },
2481
+ ],
2482
+ bySubagent: {
2483
+ primary: 156,
2484
+ "research-agent": 58,
2485
+ "content-writer": 33,
2486
+ },
2487
+ };
2488
+
2489
+ const displayStats = hasData ? stats : previewStats;
2490
+
2491
+ return (
2492
+ <div className="space-y-6">
2493
+ {/* Preview Banner */}
2494
+ {!hasData && (
2495
+ <div className="bg-[#ef4444]/10 border border-[#ef4444]/30 rounded-xl p-4 flex items-center gap-3">
2496
+ <AlertCircle className="w-5 h-5 text-[#ef4444] flex-shrink-0" />
2497
+ <div>
2498
+ <p className="font-medium text-[#ef4444]">Preview Mode</p>
2499
+ <p className="text-sm text-[var(--text-muted)]">This is sample data. Real search analytics will appear once your agents start searching for APIs.</p>
2500
+ </div>
2501
+ </div>
2502
+ )}
2503
+
2504
+ {/* Time Filter */}
2505
+ <div className="flex items-center gap-3">
2506
+ <select
2507
+ value={hoursBack}
2508
+ onChange={(e) => setHoursBack(Number(e.target.value))}
2509
+ className="px-3 py-2 rounded-lg border border-[var(--border)] bg-[var(--background)] text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
2510
+ >
2511
+ <option value={1}>Last hour</option>
2512
+ <option value={6}>Last 6 hours</option>
2513
+ <option value={24}>Last 24 hours</option>
2514
+ <option value={168}>Last 7 days</option>
2515
+ <option value={720}>Last 30 days</option>
2516
+ </select>
2517
+ </div>
2518
+
2519
+ {/* Stats Cards */}
2520
+ <div className="grid grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4">
2521
+ <div className="rounded-xl sm:rounded-2xl border border-[#ef4444]/30 bg-[#ef4444]/10 p-3 sm:p-5">
2522
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
2523
+ <span className="text-xs sm:text-sm text-[var(--text-muted)]">Total Searches</span>
2524
+ <Search className="w-4 h-4 sm:w-5 sm:h-5 text-[#ef4444]" />
2525
+ </div>
2526
+ <span className="text-xl sm:text-3xl font-bold text-[#ef4444]">{displayStats.totalSearches.toLocaleString()}</span>
2527
+ </div>
2528
+
2529
+ <div className={`rounded-xl sm:rounded-2xl border p-3 sm:p-5 ${
2530
+ displayStats.zeroResultRate > 20
2531
+ ? "border-red-500/30 bg-red-500/10"
2532
+ : "border-[var(--border)] bg-[var(--surface-elevated)]"
2533
+ }`}>
2534
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
2535
+ <span className="text-xs sm:text-sm text-[var(--text-muted)]">Zero-Result Rate</span>
2536
+ <AlertCircle className={`w-4 h-4 sm:w-5 sm:h-5 ${displayStats.zeroResultRate > 20 ? "text-red-500" : "text-[var(--text-muted)]"}`} />
2537
+ </div>
2538
+ <span className={`text-xl sm:text-3xl font-bold ${displayStats.zeroResultRate > 20 ? "text-red-500" : ""}`}>
2539
+ {displayStats.zeroResultRate.toFixed(1)}%
2540
+ </span>
2541
+ </div>
2542
+
2543
+ <div className="rounded-xl sm:rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-3 sm:p-5">
2544
+ <div className="flex items-center justify-between mb-2 sm:mb-3">
2545
+ <span className="text-xs sm:text-sm text-[var(--text-muted)]">Avg Response Time</span>
2546
+ <Clock className="w-4 h-4 sm:w-5 sm:h-5 text-[var(--text-muted)]" />
2547
+ </div>
2548
+ <span className="text-xl sm:text-3xl font-bold">{displayStats.avgResponseTime}ms</span>
2549
+ </div>
2550
+ </div>
2551
+
2552
+ {/* Two Column Layout */}
2553
+ <div className="grid lg:grid-cols-2 gap-6">
2554
+ {/* Top Queries */}
2555
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
2556
+ <h3 className="font-semibold mb-4 flex items-center gap-2">
2557
+ <TrendingUp className="w-5 h-5 text-[#ef4444]" />
2558
+ Top Queries
2559
+ </h3>
2560
+ {displayStats.topQueries.length > 0 ? (
2561
+ <div className="space-y-3">
2562
+ {displayStats.topQueries.slice(0, 10).map((item, i) => (
2563
+ <div key={item.query} className="flex items-center justify-between p-3 rounded-lg bg-[var(--surface)]">
2564
+ <div className="flex items-center gap-3">
2565
+ <span className="w-6 h-6 rounded-full bg-[#ef4444]/20 text-[#ef4444] flex items-center justify-center text-xs font-medium">
2566
+ {i + 1}
2567
+ </span>
2568
+ <code className="text-sm font-mono">{item.query}</code>
2569
+ </div>
2570
+ <span className="text-sm text-[var(--text-muted)]">{item.count}</span>
2571
+ </div>
2572
+ ))}
2573
+ </div>
2574
+ ) : (
2575
+ <p className="text-[var(--text-muted)] text-sm text-center py-4">No queries yet</p>
2576
+ )}
2577
+ </div>
2578
+
2579
+ {/* Zero-Result Queries */}
2580
+ <div className="rounded-2xl border border-red-500/30 bg-red-500/5 p-6">
2581
+ <h3 className="font-semibold mb-4 flex items-center gap-2 text-red-500">
2582
+ <AlertCircle className="w-5 h-5" />
2583
+ Zero-Result Queries
2584
+ <span className="text-xs font-normal text-[var(--text-muted)] ml-2">API Gap Opportunities</span>
2585
+ </h3>
2586
+ {displayStats.topZeroResults.length > 0 ? (
2587
+ <div className="space-y-3">
2588
+ {displayStats.topZeroResults.slice(0, 10).map((item) => (
2589
+ <div key={item.query} className="flex items-center justify-between p-3 rounded-lg bg-[var(--surface)]">
2590
+ <div className="flex items-center gap-3">
2591
+ <span className="w-2 h-2 rounded-full bg-red-500" />
2592
+ <code className="text-sm font-mono">{item.query}</code>
2593
+ </div>
2594
+ <div className="flex items-center gap-3">
2595
+ <span className="text-sm text-[var(--text-muted)]">{item.count}x</span>
2596
+ <a
2597
+ href={`/providers/register?suggested=${encodeURIComponent(item.query)}`}
2598
+ className="px-2 py-1 rounded bg-[#ef4444] text-white text-xs font-medium hover:bg-[#dc2626] transition"
2599
+ >
2600
+ Request API
2601
+ </a>
2602
+ </div>
2603
+ </div>
2604
+ ))}
2605
+ </div>
2606
+ ) : (
2607
+ <p className="text-[var(--text-muted)] text-sm text-center py-4">No zero-result queries</p>
2608
+ )}
2609
+ </div>
2610
+ </div>
2611
+
2612
+ {/* Search by Agent */}
2613
+ {Object.keys(displayStats.bySubagent).length > 0 && (
2614
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
2615
+ <h3 className="font-semibold mb-4 flex items-center gap-2">
2616
+ <Users className="w-5 h-5 text-[#ef4444]" />
2617
+ Searches by Agent
2618
+ </h3>
2619
+ <div className="grid md:grid-cols-3 gap-4">
2620
+ {Object.entries(displayStats.bySubagent).map(([agent, count]) => (
2621
+ <div key={agent} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
2622
+ <div className="flex items-center gap-3">
2623
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center ${
2624
+ agent === "primary" ? "bg-[#ef4444]/20" : "bg-[var(--background)]"
2625
+ }`}>
2626
+ {agent === "primary" ? (
2627
+ <Cpu className="w-4 h-4 text-[#ef4444]" />
2628
+ ) : (
2629
+ <Users className="w-4 h-4 text-[var(--text-muted)]" />
2630
+ )}
2631
+ </div>
2632
+ <span className="font-mono text-sm">{agent}</span>
2633
+ </div>
2634
+ <span className="text-lg font-semibold">{count}</span>
2635
+ </div>
2636
+ ))}
2637
+ </div>
2638
+ </div>
2639
+ )}
2640
+
2641
+ {/* Recent Searches */}
2642
+ <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] overflow-hidden">
2643
+ <div className="p-4 border-b border-[var(--border)]">
2644
+ <h3 className="font-semibold flex items-center gap-2">
2645
+ <Activity className="w-5 h-5 text-[#ef4444]" />
2646
+ Recent Searches
2647
+ </h3>
2648
+ </div>
2649
+
2650
+ {recentSearches.length > 0 || !hasData ? (
2651
+ <>
2652
+ {/* Desktop Table */}
2653
+ <div className="hidden md:block overflow-x-auto">
2654
+ <table className="w-full">
2655
+ <thead className="bg-[var(--surface)]">
2656
+ <tr>
2657
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Time</th>
2658
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Agent</th>
2659
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Query</th>
2660
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Results</th>
2661
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Latency</th>
2662
+ </tr>
2663
+ </thead>
2664
+ <tbody className="divide-y divide-[var(--border)]">
2665
+ {(hasData ? recentSearches : [
2666
+ { _id: "1", timestamp: Date.now() - 120000, subagentId: undefined, query: "send sms", resultCount: 3, hasResults: true, responseTimeMs: 67 },
2667
+ { _id: "2", timestamp: Date.now() - 300000, subagentId: "research-agent", query: "web search api", resultCount: 5, hasResults: true, responseTimeMs: 82 },
2668
+ { _id: "3", timestamp: Date.now() - 600000, subagentId: undefined, query: "video editing", resultCount: 0, hasResults: false, responseTimeMs: 45 },
2669
+ { _id: "4", timestamp: Date.now() - 900000, subagentId: "content-writer", query: "image generation", resultCount: 4, hasResults: true, responseTimeMs: 91 },
2670
+ { _id: "5", timestamp: Date.now() - 1200000, subagentId: undefined, query: "email service", resultCount: 2, hasResults: true, responseTimeMs: 58 },
2671
+ ]).slice(0, 20).map((search) => (
2672
+ <tr key={search._id} className="hover:bg-[var(--surface)] transition">
2673
+ <td className="px-4 py-3 text-sm text-[var(--text-muted)]">
2674
+ {formatTime(search.timestamp)}
2675
+ </td>
2676
+ <td className="px-4 py-3">
2677
+ <span className="font-mono text-sm">
2678
+ {search.subagentId || "primary"}
2679
+ </span>
2680
+ </td>
2681
+ <td className="px-4 py-3">
2682
+ <code className="px-2 py-1 rounded bg-[var(--surface)] text-sm font-mono">
2683
+ {search.query}
2684
+ </code>
2685
+ </td>
2686
+ <td className="px-4 py-3">
2687
+ {search.hasResults ? (
2688
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
2689
+ <Check className="w-3 h-3" />
2690
+ {search.resultCount} found
2691
+ </span>
2692
+ ) : (
2693
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-red-500/20 text-red-500 text-xs font-medium">
2694
+ <AlertCircle className="w-3 h-3" />
2695
+ No results
2696
+ </span>
2697
+ )}
2698
+ </td>
2699
+ <td className="px-4 py-3 text-sm">
2700
+ <span className={search.responseTimeMs > 200 ? "text-yellow-500" : "text-[var(--text-muted)]"}>
2701
+ {search.responseTimeMs}ms
2702
+ </span>
2703
+ </td>
2704
+ </tr>
2705
+ ))}
2706
+ </tbody>
2707
+ </table>
2708
+ </div>
2709
+
2710
+ {/* Mobile Cards */}
2711
+ <div className="md:hidden divide-y divide-[var(--border)]">
2712
+ {(hasData ? recentSearches : [
2713
+ { _id: "1", timestamp: Date.now() - 120000, subagentId: undefined, query: "send sms", resultCount: 3, hasResults: true, responseTimeMs: 67 },
2714
+ { _id: "2", timestamp: Date.now() - 300000, subagentId: "research-agent", query: "web search api", resultCount: 5, hasResults: true, responseTimeMs: 82 },
2715
+ { _id: "3", timestamp: Date.now() - 600000, subagentId: undefined, query: "video editing", resultCount: 0, hasResults: false, responseTimeMs: 45 },
2716
+ ]).slice(0, 10).map((search) => (
2717
+ <div key={search._id} className="p-4 space-y-2">
2718
+ <div className="flex items-center justify-between">
2719
+ <code className="font-mono text-sm font-medium">{search.query}</code>
2720
+ {search.hasResults ? (
2721
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
2722
+ {search.resultCount} found
2723
+ </span>
2724
+ ) : (
2725
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-red-500/20 text-red-500 text-xs font-medium">
2726
+ No results
2727
+ </span>
2728
+ )}
2729
+ </div>
2730
+ <div className="flex items-center justify-between text-sm text-[var(--text-muted)]">
2731
+ <span>{search.subagentId || "primary"}</span>
2732
+ <span>{search.responseTimeMs}ms</span>
2733
+ </div>
2734
+ <p className="text-xs text-[var(--text-muted)]">{formatTime(search.timestamp)}</p>
2735
+ </div>
2736
+ ))}
2737
+ </div>
2738
+ </>
2739
+ ) : (
2740
+ <div className="p-12 text-center">
2741
+ <Search className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4" />
2742
+ <h3 className="font-semibold text-lg mb-2">No searches yet</h3>
2743
+ <p className="text-[var(--text-muted)]">
2744
+ Search activity will appear here when your agents start searching for APIs.
2745
+ </p>
2746
+ </div>
2747
+ )}
2748
+ </div>
2749
+ </div>
2750
+ );
2751
+ }
2752
+
2753
+ // ============================================
2754
+ // CHAINS TAB (Chain Execution Traces)
2755
+ // ============================================
2756
+
2757
+ interface ChainExecution {
2758
+ _id: string;
2759
+ status: "pending" | "running" | "completed" | "failed" | "paused";
2760
+ currentStep: number;
2761
+ stepsCount: number;
2762
+ totalCostCents: number;
2763
+ totalLatencyMs: number;
2764
+ error?: { stepId: string; code: string; message: string };
2765
+ canResume?: boolean;
2766
+ createdAt: number;
2767
+ startedAt?: number;
2768
+ completedAt?: number;
2769
+ }
2770
+
2771
+ interface ChainStep {
2772
+ _id: string;
2773
+ stepId: string;
2774
+ stepIndex: number;
2775
+ status: "pending" | "running" | "completed" | "failed" | "skipped";
2776
+ input?: any;
2777
+ output?: any;
2778
+ latencyMs?: number;
2779
+ costCents?: number;
2780
+ error?: { code: string; message: string; retryCount?: number };
2781
+ parallelGroup?: string;
2782
+ startedAt?: number;
2783
+ completedAt?: number;
2784
+ }
2785
+
2786
+ interface ChainDetail {
2787
+ chain: {
2788
+ _id: string;
2789
+ status: string;
2790
+ steps: any[];
2791
+ totalCostCents: number;
2792
+ totalLatencyMs: number;
2793
+ startedAt?: number;
2794
+ completedAt?: number;
2795
+ };
2796
+ executions: ChainStep[];
2797
+ tokensSaved: number;
2798
+ }
2799
+
2800
+ function ChainsTab({ sessionToken }: { sessionToken: string | null }) {
2801
+ const [chains, setChains] = useState<ChainExecution[]>([]);
2802
+ const [loading, setLoading] = useState(true);
2803
+ const [statusFilter, setStatusFilter] = useState<string>("all");
2804
+ const [expandedChainId, setExpandedChainId] = useState<string | null>(null);
2805
+ const [chainDetail, setChainDetail] = useState<ChainDetail | null>(null);
2806
+ const [loadingDetail, setLoadingDetail] = useState(false);
2807
+ const [stats, setStats] = useState<{ total: number; completed: number; failed: number; running: number; successRate: number; totalCostCents: number } | null>(null);
2808
+
2809
+ const fetchChains = useCallback(async () => {
2810
+ if (!sessionToken) return;
2811
+ try {
2812
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
2813
+ method: "POST",
2814
+ headers: { "Content-Type": "application/json" },
2815
+ body: JSON.stringify({
2816
+ path: "chains:getChainExecutions",
2817
+ args: { token: sessionToken, limit: 50, status: statusFilter },
2818
+ }),
2819
+ });
2820
+ const data = await res.json();
2821
+ const result = data.value || data;
2822
+ if (Array.isArray(result)) setChains(result);
2823
+ } catch (err) {
2824
+ console.error("Fetch chains error:", err);
2825
+ } finally {
2826
+ setLoading(false);
2827
+ }
2828
+ }, [sessionToken, statusFilter]);
2829
+
2830
+ const fetchStats = useCallback(async () => {
2831
+ if (!sessionToken) return;
2832
+ try {
2833
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
2834
+ method: "POST",
2835
+ headers: { "Content-Type": "application/json" },
2836
+ body: JSON.stringify({
2837
+ path: "chains:getChainStatsAuth",
2838
+ args: { token: sessionToken },
2839
+ }),
2840
+ });
2841
+ const data = await res.json();
2842
+ const result = data.value || data;
2843
+ if (result && !result.error) setStats(result);
2844
+ } catch (err) {
2845
+ console.error("Fetch stats error:", err);
2846
+ }
2847
+ }, [sessionToken]);
2848
+
2849
+ const fetchChainDetail = useCallback(async (chainId: string) => {
2850
+ if (!sessionToken) return;
2851
+ setLoadingDetail(true);
2852
+ try {
2853
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
2854
+ method: "POST",
2855
+ headers: { "Content-Type": "application/json" },
2856
+ body: JSON.stringify({
2857
+ path: "chains:getChainTraceAuth",
2858
+ args: { token: sessionToken, chainId },
2859
+ }),
2860
+ });
2861
+ const data = await res.json();
2862
+ const result = data.value || data;
2863
+ if (result && !result.error) setChainDetail(result);
2864
+ } catch (err) {
2865
+ console.error("Fetch chain detail error:", err);
2866
+ } finally {
2867
+ setLoadingDetail(false);
2868
+ }
2869
+ }, [sessionToken]);
2870
+
2871
+ const handleResume = async (chainId: string) => {
2872
+ if (!sessionToken) return;
2873
+ try {
2874
+ await fetch(`${CONVEX_URL}/api/mutation`, {
2875
+ method: "POST",
2876
+ headers: { "Content-Type": "application/json" },
2877
+ body: JSON.stringify({
2878
+ path: "chains:resumeChainAuth",
2879
+ args: { token: sessionToken, chainId },
2880
+ }),
2881
+ });
2882
+ fetchChains();
2883
+ } catch (err) {
2884
+ console.error("Resume chain error:", err);
2885
+ }
2886
+ };
2887
+
2888
+ useEffect(() => {
2889
+ fetchChains();
2890
+ fetchStats();
2891
+ }, [fetchChains, fetchStats]);
2892
+
2893
+ useEffect(() => {
2894
+ if (expandedChainId) {
2895
+ fetchChainDetail(expandedChainId);
2896
+ } else {
2897
+ setChainDetail(null);
2898
+ }
2899
+ }, [expandedChainId, fetchChainDetail]);
2900
+
2901
+ const getStatusIcon = (status: string) => {
2902
+ switch (status) {
2903
+ case "completed": return <Check className="w-4 h-4 text-green-500" />;
2904
+ case "running": return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
2905
+ case "failed": return <AlertCircle className="w-4 h-4 text-red-500" />;
2906
+ case "paused": return <Clock className="w-4 h-4 text-yellow-500" />;
2907
+ default: return <Clock className="w-4 h-4 text-gray-500" />;
2908
+ }
2909
+ };
2910
+
2911
+ const formatDuration = (ms: number) => ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
2912
+ const formatCost = (cents: number) => cents === 0 ? "$0.00" : `$${(cents / 100).toFixed(2)}`;
2913
+ const formatTime = (ts: number) => {
2914
+ const diff = Date.now() - ts;
2915
+ if (diff < 60000) return "just now";
2916
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
2917
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
2918
+ return new Date(ts).toLocaleDateString();
2919
+ };
2920
+
2921
+ if (loading) {
2922
+ return (
2923
+ <div className="flex items-center justify-center py-12">
2924
+ <Loader2 className="w-8 h-8 text-[#ef4444] animate-spin" />
2925
+ </div>
2926
+ );
2927
+ }
1679
2928
 
1680
2929
  return (
1681
2930
  <div className="space-y-6">
1682
- {/* Subtab Navigation */}
1683
- <div className="flex items-center gap-1 p-1 bg-[var(--surface)] rounded-xl w-fit">
1684
- <button
1685
- onClick={() => {
1686
- setActiveSubtab("overview");
1687
- router.push("/workspace?tab=analytics&sub=overview");
1688
- }}
1689
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
1690
- activeSubtab === "overview"
1691
- ? "bg-[#ef4444] text-white"
1692
- : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1693
- }`}
1694
- >
1695
- <BarChart3 className="w-4 h-4" />
1696
- Overview
1697
- </button>
1698
- <button
1699
- onClick={() => {
1700
- setActiveSubtab("usage");
1701
- router.push("/workspace?tab=analytics&sub=usage");
1702
- }}
1703
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
1704
- activeSubtab === "usage"
1705
- ? "bg-[#ef4444] text-white"
1706
- : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1707
- }`}
1708
- >
1709
- <TrendingUp className="w-4 h-4" />
1710
- Usage
1711
- </button>
1712
- <button
1713
- onClick={() => {
1714
- setActiveSubtab("logs");
1715
- router.push("/workspace?tab=analytics&sub=logs");
1716
- }}
1717
- className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
1718
- activeSubtab === "logs"
1719
- ? "bg-[#ef4444] text-white"
1720
- : "text-[var(--text-muted)] hover:text-[var(--text-primary)]"
1721
- }`}
2931
+ {/* Stats Cards */}
2932
+ {stats && (
2933
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
2934
+ <StatCard title="Total Chains" value={stats.total.toString()} icon={Zap} />
2935
+ <StatCard title="Success Rate" value={`${stats.successRate}%`} icon={Check} accent={stats.successRate >= 90} />
2936
+ <StatCard title="Running" value={stats.running.toString()} icon={Activity} />
2937
+ <StatCard title="Total Cost" value={formatCost(stats.totalCostCents)} icon={CreditCard} />
2938
+ </div>
2939
+ )}
2940
+
2941
+ {/* Filter */}
2942
+ <div className="flex items-center gap-3">
2943
+ <select
2944
+ value={statusFilter}
2945
+ onChange={(e) => setStatusFilter(e.target.value)}
2946
+ className="bg-[var(--surface)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#ef4444]/50"
1722
2947
  >
1723
- <ScrollText className="w-4 h-4" />
1724
- Logs
1725
- </button>
2948
+ <option value="all">All Status</option>
2949
+ <option value="running">Running</option>
2950
+ <option value="completed">Completed</option>
2951
+ <option value="failed">Failed</option>
2952
+ <option value="paused">Paused</option>
2953
+ </select>
2954
+ <span className="text-[var(--text-muted)] text-sm">{chains.length} chain{chains.length !== 1 ? "s" : ""}</span>
1726
2955
  </div>
1727
2956
 
1728
- {/* Subtab Content */}
1729
- {activeSubtab === "overview" && (
1730
- <AnalyticsOverviewTab apis={apis} analytics={analytics} workspace={workspace} agents={agents} usage={usage} />
1731
- )}
1732
- {activeSubtab === "usage" && (
1733
- <UsageTab workspace={workspace} usage={usage} />
1734
- )}
1735
- {activeSubtab === "logs" && (
1736
- <LogsTab sessionToken={sessionToken} />
2957
+ {/* Chains List */}
2958
+ {chains.length === 0 ? (
2959
+ <div className="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-12 text-center">
2960
+ <Activity className="w-12 h-12 text-[var(--text-muted)] mx-auto mb-4 opacity-50" />
2961
+ <h3 className="text-lg font-medium mb-2">No Chain Executions Yet</h3>
2962
+ <p className="text-[var(--text-muted)] max-w-md mx-auto">
2963
+ Chain executions will appear here when you start orchestrating multi-step API workflows.
2964
+ </p>
2965
+ </div>
2966
+ ) : (
2967
+ <div className="space-y-3">
2968
+ {chains.map((chain) => (
2969
+ <div
2970
+ key={chain._id}
2971
+ className={`bg-[var(--surface)] rounded-xl border transition-all ${
2972
+ expandedChainId === chain._id ? "border-[#ef4444]/50" : "border-[var(--border)] hover:border-[var(--border-hover)]"
2973
+ }`}
2974
+ >
2975
+ {/* Chain Row */}
2976
+ <button
2977
+ onClick={() => setExpandedChainId(expandedChainId === chain._id ? null : chain._id)}
2978
+ className="w-full p-4 flex items-center justify-between text-left"
2979
+ >
2980
+ <div className="flex items-center gap-4">
2981
+ {expandedChainId === chain._id ? <ChevronDown className="w-4 h-4 text-[var(--text-muted)]" /> : <ChevronRight className="w-4 h-4 text-[var(--text-muted)]" />}
2982
+ {getStatusIcon(chain.status)}
2983
+ <span className="text-sm font-medium capitalize">{chain.status}</span>
2984
+ <code className="text-xs text-[var(--text-muted)] font-mono">{chain._id.slice(0, 12)}...</code>
2985
+ </div>
2986
+ <div className="flex items-center gap-6 text-sm text-[var(--text-muted)]">
2987
+ <span>{chain.stepsCount} steps</span>
2988
+ <span>{formatDuration(chain.totalLatencyMs)}</span>
2989
+ <span>{formatCost(chain.totalCostCents)}</span>
2990
+ <span>{formatTime(chain.createdAt)}</span>
2991
+ </div>
2992
+ </button>
2993
+
2994
+ {/* Expanded Detail */}
2995
+ {expandedChainId === chain._id && (
2996
+ <div className="border-t border-[var(--border)] p-4">
2997
+ {loadingDetail ? (
2998
+ <div className="flex items-center justify-center py-8">
2999
+ <Loader2 className="w-6 h-6 text-[#ef4444] animate-spin" />
3000
+ </div>
3001
+ ) : chainDetail ? (
3002
+ <div className="space-y-4">
3003
+ {/* Actions */}
3004
+ <div className="flex items-center gap-2 pb-4 border-b border-[var(--border)]">
3005
+ {chain.canResume && (
3006
+ <button
3007
+ onClick={() => handleResume(chain._id)}
3008
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#ef4444] hover:bg-[#ef4444]/80 text-white text-sm font-medium transition-colors"
3009
+ >
3010
+ <Play className="w-3.5 h-3.5" />
3011
+ Resume
3012
+ </button>
3013
+ )}
3014
+ <button
3015
+ onClick={() => navigator.clipboard.writeText(chain._id)}
3016
+ className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[var(--surface-elevated)] hover:bg-[var(--border)] text-sm transition-colors"
3017
+ >
3018
+ <Copy className="w-3.5 h-3.5" />
3019
+ Copy ID
3020
+ </button>
3021
+ </div>
3022
+
3023
+ {/* Gantt Timeline */}
3024
+ <div className="bg-[var(--background)] rounded-xl border border-[var(--border)] p-4">
3025
+ <div className="flex items-center justify-between mb-4">
3026
+ <span className="text-sm font-medium">Execution Timeline</span>
3027
+ <span className="text-xs text-[var(--text-muted)]">
3028
+ Total: {formatDuration(chainDetail.chain.totalLatencyMs)} • Cost: {formatCost(chainDetail.chain.totalCostCents)} • Tokens Saved: ~{chainDetail.tokensSaved.toLocaleString()}
3029
+ </span>
3030
+ </div>
3031
+ <div className="space-y-2">
3032
+ {chainDetail.executions.map((step) => {
3033
+ const totalMs = chainDetail.chain.totalLatencyMs || 1;
3034
+ const widthPct = Math.max(5, ((step.latencyMs || 0) / totalMs) * 100);
3035
+ return (
3036
+ <div key={step._id} className="flex items-center gap-3">
3037
+ <div className="w-24 flex items-center gap-2 flex-shrink-0">
3038
+ {getStatusIcon(step.status)}
3039
+ <span className="text-xs font-mono truncate">{step.stepId}</span>
3040
+ </div>
3041
+ <div className="flex-1 h-5 bg-[var(--surface)] rounded relative overflow-hidden">
3042
+ <div
3043
+ className={`absolute left-0 top-0 h-full rounded ${
3044
+ step.status === "completed" ? "bg-green-500" :
3045
+ step.status === "running" ? "bg-blue-500 animate-pulse" :
3046
+ step.status === "failed" ? "bg-red-500" : "bg-gray-500"
3047
+ }`}
3048
+ style={{ width: `${widthPct}%` }}
3049
+ />
3050
+ <span className="absolute left-2 top-0.5 text-xs font-mono text-white drop-shadow-sm">
3051
+ {formatDuration(step.latencyMs || 0)}
3052
+ </span>
3053
+ </div>
3054
+ <span className="w-14 text-right text-xs text-[var(--text-muted)]">
3055
+ {formatCost(step.costCents || 0)}
3056
+ </span>
3057
+ </div>
3058
+ );
3059
+ })}
3060
+ </div>
3061
+ {/* Legend */}
3062
+ <div className="flex items-center gap-4 mt-4 pt-3 border-t border-[var(--border)] text-xs text-[var(--text-muted)]">
3063
+ <div className="flex items-center gap-1"><div className="w-3 h-3 rounded bg-green-500" /> Completed</div>
3064
+ <div className="flex items-center gap-1"><div className="w-3 h-3 rounded bg-blue-500" /> Running</div>
3065
+ <div className="flex items-center gap-1"><div className="w-3 h-3 rounded bg-red-500" /> Failed</div>
3066
+ <div className="flex items-center gap-1"><div className="w-3 h-3 rounded bg-yellow-500" /> Paused</div>
3067
+ </div>
3068
+ </div>
3069
+
3070
+ {/* Error Display */}
3071
+ {chain.error && (
3072
+ <div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4">
3073
+ <div className="flex items-center gap-2 mb-2">
3074
+ <AlertCircle className="w-4 h-4 text-red-500" />
3075
+ <span className="font-medium text-red-500">Error at step: {chain.error.stepId}</span>
3076
+ <code className="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded">{chain.error.code}</code>
3077
+ </div>
3078
+ <p className="text-sm text-red-400">{chain.error.message}</p>
3079
+ </div>
3080
+ )}
3081
+ </div>
3082
+ ) : (
3083
+ <div className="text-center text-[var(--text-muted)] py-4">Failed to load chain details</div>
3084
+ )}
3085
+ </div>
3086
+ )}
3087
+ </div>
3088
+ ))}
3089
+ </div>
1737
3090
  )}
1738
3091
  </div>
1739
3092
  );
@@ -1749,16 +3102,49 @@ function AnalyticsOverviewTab({
1749
3102
  workspace,
1750
3103
  agents,
1751
3104
  usage,
3105
+ sessionToken,
1752
3106
  }: {
1753
3107
  apis: ProviderAPI[];
1754
3108
  analytics: ProviderAnalytics | null;
1755
3109
  workspace: Workspace | null;
1756
3110
  agents: Agent[];
1757
3111
  usage: UsageData | null;
3112
+ sessionToken: string | null;
1758
3113
  }) {
3114
+ const [searchStats, setSearchStats] = useState<{ totalSearches: number; zeroResultRate: number } | null>(null);
3115
+
3116
+ // Fetch search stats
3117
+ useEffect(() => {
3118
+ const fetchSearchStats = async () => {
3119
+ if (!sessionToken) return;
3120
+ try {
3121
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
3122
+ method: "POST",
3123
+ headers: { "Content-Type": "application/json" },
3124
+ body: JSON.stringify({
3125
+ path: "searchLogs:getStats",
3126
+ args: { token: sessionToken, hoursBack: 168 }, // Last 7 days
3127
+ }),
3128
+ });
3129
+ const data = await res.json();
3130
+ const result = data.value || data;
3131
+ if (result && !result.error) {
3132
+ setSearchStats({
3133
+ totalSearches: result.totalSearches || 0,
3134
+ zeroResultRate: result.zeroResultRate || 0,
3135
+ });
3136
+ }
3137
+ } catch (err) {
3138
+ console.error("Error fetching search stats:", err);
3139
+ }
3140
+ };
3141
+ fetchSearchStats();
3142
+ }, [sessionToken]);
3143
+
1759
3144
  const totalCalls = analytics?.totalCalls || workspace?.usageCount || 0;
1760
3145
  const uniqueAgents = analytics?.uniqueAgents || agents.length || 0;
1761
3146
  const hasChartData = analytics && analytics.callsByDay && analytics.callsByDay.length > 0;
3147
+ const totalSearches = searchStats?.totalSearches || (analytics?.isPreview ? 247 : 0);
1762
3148
 
1763
3149
  return (
1764
3150
  <div className="space-y-8">
@@ -1774,8 +3160,9 @@ function AnalyticsOverviewTab({
1774
3160
  )}
1775
3161
 
1776
3162
  {/* Stats Grid */}
1777
- <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4">
3163
+ <div className="grid grid-cols-2 lg:grid-cols-5 gap-3 md:gap-4">
1778
3164
  <StatCard title="Total Calls" value={totalCalls.toLocaleString()} icon={Zap} accent />
3165
+ <StatCard title="Total Searches" value={totalSearches.toLocaleString()} icon={Search} />
1779
3166
  <StatCard title="Connected Agents" value={uniqueAgents.toString()} icon={Users} />
1780
3167
  <StatCard title="Avg Latency" value={`${analytics?.avgLatency || 145}ms`} icon={Clock} />
1781
3168
  <StatCard title="Success Rate" value={`${(analytics?.successRate || 98.2).toFixed(1)}%`} icon={Check} />
@@ -1878,10 +3265,48 @@ function AnalyticsOverviewTab({
1878
3265
  function UsageTab({
1879
3266
  workspace,
1880
3267
  usage,
3268
+ sessionToken,
1881
3269
  }: {
1882
3270
  workspace: Workspace | null;
1883
3271
  usage: UsageData | null;
3272
+ sessionToken: string | null;
1884
3273
  }) {
3274
+ const [searchStats, setSearchStats] = useState<{
3275
+ totalSearches: number;
3276
+ searchesByProvider: Record<string, number>;
3277
+ } | null>(null);
3278
+
3279
+ // Fetch search stats to correlate with API usage
3280
+ useEffect(() => {
3281
+ const fetchSearchStats = async () => {
3282
+ if (!sessionToken) return;
3283
+ try {
3284
+ const res = await fetch(`${CONVEX_URL}/api/query`, {
3285
+ method: "POST",
3286
+ headers: { "Content-Type": "application/json" },
3287
+ body: JSON.stringify({
3288
+ path: "searchLogs:getStats",
3289
+ args: { token: sessionToken, hoursBack: 720 }, // Last 30 days
3290
+ }),
3291
+ });
3292
+ const data = await res.json();
3293
+ const result = data.value || data;
3294
+ if (result && !result.error) {
3295
+ // Build provider search counts from top queries that matched providers
3296
+ const searchesByProvider: Record<string, number> = {};
3297
+ // Estimate based on result counts - in real implementation this would come from matchedProviders
3298
+ setSearchStats({
3299
+ totalSearches: result.totalSearches || 0,
3300
+ searchesByProvider,
3301
+ });
3302
+ }
3303
+ } catch (err) {
3304
+ console.error("Error fetching search stats:", err);
3305
+ }
3306
+ };
3307
+ fetchSearchStats();
3308
+ }, [sessionToken]);
3309
+
1885
3310
  const hasRealData = usage && (usage.byProvider.length > 0 || usage.byDay.length > 0);
1886
3311
 
1887
3312
  // Preview data for empty state (provider perspective - how others use YOUR APIs)
@@ -1897,15 +3322,18 @@ function UsageTab({
1897
3322
 
1898
3323
  // Preview shows YOUR listed APIs and agents using them
1899
3324
  const previewByApi = [
1900
- { provider: "Your API Name", calls: 89, cost: 4.45 },
1901
- { provider: "Another API", calls: 42, cost: 2.10 },
1902
- { provider: "Third API", calls: 17, cost: 0.85 },
3325
+ { provider: "46elks", calls: 847, cost: 42.35, searchCount: 12 },
3326
+ { provider: "openrouter", calls: 623, cost: 31.15, searchCount: 45 },
3327
+ { provider: "replicate", calls: 512, cost: 25.60, searchCount: 8 },
1903
3328
  ];
1904
3329
 
1905
3330
  const isPreview = !hasRealData;
1906
3331
  const displayByDay = hasRealData ? usage!.byDay : previewByDay;
1907
- const displayByProvider = hasRealData ? usage!.byProvider : previewByApi;
1908
- const displayTotal = hasRealData ? (usage?.total || workspace?.usageCount || 0) : 148;
3332
+ const displayByProvider = hasRealData
3333
+ ? usage!.byProvider.map(p => ({ ...p, searchCount: searchStats?.searchesByProvider[p.provider] || 0 }))
3334
+ : previewByApi;
3335
+ const displayTotal = hasRealData ? (usage?.total || workspace?.usageCount || 0) : 1982;
3336
+ const displaySearchTotal = searchStats?.totalSearches || (isPreview ? 156 : 0);
1909
3337
 
1910
3338
  return (
1911
3339
  <div className="space-y-8">
@@ -1920,7 +3348,8 @@ function UsageTab({
1920
3348
  </div>
1921
3349
  )}
1922
3350
 
1923
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-3 md:gap-4">
3351
+ {/* Stats Grid - Now with 4 cards including Search */}
3352
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 md:gap-4">
1924
3353
  <div className="rounded-2xl border border-[#ef4444]/30 bg-[#ef4444]/10 p-4 sm:p-6">
1925
3354
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
1926
3355
  <Zap className="w-5 h-5 sm:w-6 sm:h-6 text-[#ef4444]" />
@@ -1934,17 +3363,27 @@ function UsageTab({
1934
3363
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
1935
3364
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
1936
3365
  <TrendingUp className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-muted)]" />
1937
- <span className="text-sm sm:text-base text-[var(--text-muted)]">Your APIs</span>
3366
+ <span className="text-sm sm:text-base text-[var(--text-muted)]">Unique APIs</span>
1938
3367
  </div>
1939
3368
  <p className="text-2xl sm:text-4xl font-bold">{displayByProvider.length}</p>
1940
3369
  </div>
1941
3370
 
3371
+ <div className="rounded-2xl border border-blue-500/30 bg-blue-500/10 p-4 sm:p-6">
3372
+ <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
3373
+ <Search className="w-5 h-5 sm:w-6 sm:h-6 text-blue-500" />
3374
+ <span className="text-sm sm:text-base text-[var(--text-muted)]">Found via Search</span>
3375
+ </div>
3376
+ <p className="text-2xl sm:text-4xl font-bold text-blue-500">
3377
+ {displaySearchTotal.toLocaleString()}
3378
+ </p>
3379
+ </div>
3380
+
1942
3381
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-4 sm:p-6">
1943
3382
  <div className="flex items-center gap-2 sm:gap-3 mb-2 sm:mb-3">
1944
3383
  <Users className="w-5 h-5 sm:w-6 sm:h-6 text-[var(--text-muted)]" />
1945
3384
  <span className="text-sm sm:text-base text-[var(--text-muted)]">Unique Agents</span>
1946
3385
  </div>
1947
- <p className="text-2xl sm:text-4xl font-bold">{isPreview ? "12" : "0"}</p>
3386
+ <p className="text-2xl sm:text-4xl font-bold">{isPreview ? "23" : "0"}</p>
1948
3387
  </div>
1949
3388
  </div>
1950
3389
 
@@ -1974,9 +3413,9 @@ function UsageTab({
1974
3413
  </div>
1975
3414
  </div>
1976
3415
 
1977
- {/* Calls to Your APIs */}
3416
+ {/* Top APIs - Now with Search column */}
1978
3417
  <div className="rounded-2xl border border-[var(--border)] bg-[var(--surface-elevated)] p-6">
1979
- <h3 className="font-semibold mb-4">Calls to Your APIs</h3>
3418
+ <h3 className="font-semibold mb-4">Top APIs</h3>
1980
3419
  <div className="space-y-3">
1981
3420
  {displayByProvider.map((p, i) => (
1982
3421
  <div key={p.provider} className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
@@ -1984,11 +3423,21 @@ function UsageTab({
1984
3423
  <span className="w-8 h-8 rounded-full bg-[#ef4444]/20 text-[#ef4444] flex items-center justify-center text-sm font-medium">
1985
3424
  {i + 1}
1986
3425
  </span>
1987
- <span className="font-medium">{p.provider}</span>
3426
+ <div>
3427
+ <span className="font-medium">{p.provider}</span>
3428
+ <div className="flex items-center gap-2 mt-1">
3429
+ <span className="text-sm text-[var(--text-muted)]">{p.calls.toLocaleString()} calls</span>
3430
+ {p.cost > 0 && <span className="text-sm text-[var(--text-muted)]">• ${p.cost.toFixed(2)}</span>}
3431
+ </div>
3432
+ </div>
1988
3433
  </div>
1989
- <div className="text-right">
1990
- <p className="font-semibold">{p.calls.toLocaleString()} calls</p>
1991
- {p.cost > 0 && <p className="text-sm text-[var(--text-muted)]">${p.cost.toFixed(2)}</p>}
3434
+ <div className="flex items-center gap-2">
3435
+ {(p as any).searchCount > 0 && (
3436
+ <span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-blue-500/20 text-blue-500 text-xs font-medium">
3437
+ <Search className="w-3 h-3" />
3438
+ {(p as any).searchCount} found
3439
+ </span>
3440
+ )}
1992
3441
  </div>
1993
3442
  </div>
1994
3443
  ))}
@@ -2002,8 +3451,39 @@ function UsageTab({
2002
3451
  // LOGS TAB
2003
3452
  // ============================================
2004
3453
 
2005
- interface LogEntry {
3454
+ // Type badges for log entries
3455
+ const typeBadges: Record<string, { icon: typeof Search; label: string; className: string }> = {
3456
+ search: {
3457
+ icon: Search,
3458
+ label: "Search",
3459
+ className: "bg-blue-500/10 text-blue-500 border border-blue-500/20"
3460
+ },
3461
+ direct_call: {
3462
+ icon: Zap,
3463
+ label: "Direct Call",
3464
+ className: "bg-green-500/10 text-green-500 border border-green-500/20"
3465
+ },
3466
+ chain: {
3467
+ icon: LinkIcon,
3468
+ label: "Chain",
3469
+ className: "bg-purple-500/10 text-purple-500 border border-purple-500/20"
3470
+ },
3471
+ };
3472
+
3473
+ const TypeBadge = ({ type }: { type: string }) => {
3474
+ const badge = typeBadges[type] || typeBadges.direct_call;
3475
+ const Icon = badge.icon;
3476
+ return (
3477
+ <span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium ${badge.className}`}>
3478
+ <Icon className="w-3.5 h-3.5" />
3479
+ {badge.label}
3480
+ </span>
3481
+ );
3482
+ };
3483
+
3484
+ interface ApiLogEntry {
2006
3485
  id: string;
3486
+ type: "direct_call";
2007
3487
  provider: string;
2008
3488
  action: string;
2009
3489
  status: "success" | "error";
@@ -2012,6 +3492,18 @@ interface LogEntry {
2012
3492
  createdAt: number;
2013
3493
  }
2014
3494
 
3495
+ interface SearchLogEntry {
3496
+ id: string;
3497
+ type: "search";
3498
+ query: string;
3499
+ resultCount: number;
3500
+ hasResults: boolean;
3501
+ responseTimeMs: number;
3502
+ createdAt: number;
3503
+ }
3504
+
3505
+ type CombinedLogEntry = ApiLogEntry | SearchLogEntry;
3506
+
2015
3507
  interface LogStats {
2016
3508
  totalCalls: number;
2017
3509
  successCount: number;
@@ -2022,7 +3514,7 @@ interface LogStats {
2022
3514
  }
2023
3515
 
2024
3516
  function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2025
- const [logs, setLogs] = useState<LogEntry[]>([]);
3517
+ const [logs, setLogs] = useState<CombinedLogEntry[]>([]);
2026
3518
  const [stats, setStats] = useState<LogStats | null>(null);
2027
3519
  const [isLoading, setIsLoading] = useState(true);
2028
3520
  const [statusFilter, setStatusFilter] = useState<"all" | "success" | "error">("all");
@@ -2044,6 +3536,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2044
3536
  try {
2045
3537
  const cursor = append ? nextCursor : undefined;
2046
3538
 
3539
+ // Fetch API logs
2047
3540
  const logsRes = await fetch(`${CONVEX_URL}/api/query`, {
2048
3541
  method: "POST",
2049
3542
  headers: { "Content-Type": "application/json" },
@@ -2060,15 +3553,52 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2060
3553
  });
2061
3554
 
2062
3555
  const logsData = await logsRes.json();
2063
- const result = logsData.value || logsData;
3556
+ const apiResult = logsData.value || logsData;
3557
+ const apiLogs: ApiLogEntry[] = (apiResult.logs || []).map((log: any) => ({
3558
+ ...log,
3559
+ type: "direct_call" as const,
3560
+ }));
3561
+
3562
+ // Fetch search logs (only on initial load, not on "load more")
3563
+ let searchLogs: SearchLogEntry[] = [];
3564
+ if (!append && providerFilter === "all") {
3565
+ try {
3566
+ const searchRes = await fetch(`${CONVEX_URL}/api/query`, {
3567
+ method: "POST",
3568
+ headers: { "Content-Type": "application/json" },
3569
+ body: JSON.stringify({
3570
+ path: "searchLogs:getRecent",
3571
+ args: { token: sessionToken, limit: 50 },
3572
+ }),
3573
+ });
3574
+ const searchData = await searchRes.json();
3575
+ const searchResult = searchData.value || searchData;
3576
+ if (Array.isArray(searchResult)) {
3577
+ searchLogs = searchResult.map((log: any) => ({
3578
+ id: log._id,
3579
+ type: "search" as const,
3580
+ query: log.query,
3581
+ resultCount: log.resultCount,
3582
+ hasResults: log.hasResults,
3583
+ responseTimeMs: log.responseTimeMs,
3584
+ createdAt: log.timestamp,
3585
+ }));
3586
+ }
3587
+ } catch (err) {
3588
+ console.error("Error fetching search logs:", err);
3589
+ }
3590
+ }
3591
+
3592
+ // Merge and sort by timestamp (newest first)
3593
+ const combinedLogs = [...apiLogs, ...searchLogs].sort((a, b) => b.createdAt - a.createdAt);
2064
3594
 
2065
3595
  if (append) {
2066
- setLogs(prev => [...prev, ...(result.logs || [])]);
3596
+ setLogs(prev => [...prev, ...apiLogs].sort((a, b) => b.createdAt - a.createdAt));
2067
3597
  } else {
2068
- setLogs(result.logs || []);
3598
+ setLogs(combinedLogs);
2069
3599
  }
2070
- setHasMore(result.hasMore || false);
2071
- setNextCursor(result.nextCursor);
3600
+ setHasMore(apiResult.hasMore || false);
3601
+ setNextCursor(apiResult.nextCursor);
2072
3602
  } catch (err) {
2073
3603
  console.error("Error fetching logs:", err);
2074
3604
  } finally {
@@ -2225,6 +3755,7 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2225
3755
  <table className="w-full">
2226
3756
  <thead className="bg-[var(--surface)]">
2227
3757
  <tr>
3758
+ <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Type</th>
2228
3759
  <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Time</th>
2229
3760
  <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Provider</th>
2230
3761
  <th className="px-4 py-3 text-left text-sm font-medium text-[var(--text-muted)]">Action</th>
@@ -2235,34 +3766,65 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2235
3766
  <tbody className="divide-y divide-[var(--border)]">
2236
3767
  {logs.map((log) => (
2237
3768
  <tr key={log.id} className="hover:bg-[var(--surface)] transition">
3769
+ <td className="px-4 py-3">
3770
+ <TypeBadge type={log.type} />
3771
+ </td>
2238
3772
  <td className="px-4 py-3 text-sm text-[var(--text-muted)]">
2239
3773
  {formatTime(log.createdAt)}
2240
3774
  </td>
2241
3775
  <td className="px-4 py-3">
2242
- <span className="font-medium">{log.provider}</span>
3776
+ {log.type === "search" ? (
3777
+ <span className="font-medium text-[var(--text-muted)]">—</span>
3778
+ ) : (
3779
+ <span className="font-medium">{(log as ApiLogEntry).provider}</span>
3780
+ )}
2243
3781
  </td>
2244
3782
  <td className="px-4 py-3">
2245
- <code className="px-2 py-1 rounded bg-[var(--surface)] text-sm font-mono">
2246
- {log.action}
2247
- </code>
3783
+ {log.type === "search" ? (
3784
+ <code className="px-2 py-1 rounded bg-blue-500/10 text-blue-500 text-sm font-mono">
3785
+ &quot;{(log as SearchLogEntry).query}&quot;
3786
+ </code>
3787
+ ) : (
3788
+ <code className="px-2 py-1 rounded bg-[var(--surface)] text-sm font-mono">
3789
+ {(log as ApiLogEntry).action}
3790
+ </code>
3791
+ )}
2248
3792
  </td>
2249
3793
  <td className="px-4 py-3">
2250
- {log.status === "success" ? (
3794
+ {log.type === "search" ? (
3795
+ (log as SearchLogEntry).hasResults ? (
3796
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
3797
+ <Check className="w-3 h-3" />
3798
+ {(log as SearchLogEntry).resultCount} results
3799
+ </span>
3800
+ ) : (
3801
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-500/20 text-yellow-500 text-xs font-medium">
3802
+ <AlertCircle className="w-3 h-3" />
3803
+ No results
3804
+ </span>
3805
+ )
3806
+ ) : (log as ApiLogEntry).status === "success" ? (
2251
3807
  <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
2252
3808
  <Check className="w-3 h-3" />
2253
3809
  Success
2254
3810
  </span>
2255
3811
  ) : (
2256
- <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-red-500/20 text-red-500 text-xs font-medium" title={log.errorMessage}>
3812
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-red-500/20 text-red-500 text-xs font-medium" title={(log as ApiLogEntry).errorMessage}>
2257
3813
  <AlertCircle className="w-3 h-3" />
2258
3814
  Error
2259
3815
  </span>
2260
3816
  )}
2261
3817
  </td>
2262
3818
  <td className="px-4 py-3 text-sm">
2263
- <span className={log.latencyMs > 1000 ? "text-yellow-500" : "text-[var(--text-muted)]"}>
2264
- {log.latencyMs}ms
2265
- </span>
3819
+ {log.type === "search" ? (
3820
+ <span className={(log as SearchLogEntry).responseTimeMs > 200 ? "text-yellow-500" : "text-[var(--text-muted)]"}>
3821
+ {(log as SearchLogEntry).responseTimeMs}ms
3822
+ </span>
3823
+ ) : (
3824
+ <span className={(log as ApiLogEntry).latencyMs > 1000 ? "text-yellow-500" : "text-[var(--text-muted)]"}>
3825
+ {(log as ApiLogEntry).latencyMs}ms
3826
+ </span>
3827
+ )}
2266
3828
  </td>
2267
3829
  </tr>
2268
3830
  ))}
@@ -2275,8 +3837,23 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2275
3837
  {logs.map((log) => (
2276
3838
  <div key={log.id} className="p-4 space-y-2">
2277
3839
  <div className="flex items-center justify-between">
2278
- <span className="font-medium">{log.provider}</span>
2279
- {log.status === "success" ? (
3840
+ <div className="flex items-center gap-2">
3841
+ <TypeBadge type={log.type} />
3842
+ {log.type === "direct_call" && (
3843
+ <span className="font-medium text-sm">{(log as ApiLogEntry).provider}</span>
3844
+ )}
3845
+ </div>
3846
+ {log.type === "search" ? (
3847
+ (log as SearchLogEntry).hasResults ? (
3848
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
3849
+ {(log as SearchLogEntry).resultCount} results
3850
+ </span>
3851
+ ) : (
3852
+ <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-500/20 text-yellow-500 text-xs font-medium">
3853
+ No results
3854
+ </span>
3855
+ )
3856
+ ) : (log as ApiLogEntry).status === "success" ? (
2280
3857
  <span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-green-500/20 text-green-500 text-xs font-medium">
2281
3858
  <Check className="w-3 h-3" />
2282
3859
  Success
@@ -2289,14 +3866,22 @@ function LogsTab({ sessionToken }: { sessionToken: string | null }) {
2289
3866
  )}
2290
3867
  </div>
2291
3868
  <div className="flex items-center justify-between text-sm">
2292
- <code className="px-2 py-1 rounded bg-[var(--surface)] font-mono text-xs">
2293
- {log.action}
2294
- </code>
2295
- <span className="text-[var(--text-muted)]">{log.latencyMs}ms</span>
3869
+ {log.type === "search" ? (
3870
+ <code className="px-2 py-1 rounded bg-blue-500/10 text-blue-500 font-mono text-xs">
3871
+ &quot;{(log as SearchLogEntry).query}&quot;
3872
+ </code>
3873
+ ) : (
3874
+ <code className="px-2 py-1 rounded bg-[var(--surface)] font-mono text-xs">
3875
+ {(log as ApiLogEntry).action}
3876
+ </code>
3877
+ )}
3878
+ <span className="text-[var(--text-muted)]">
3879
+ {log.type === "search" ? (log as SearchLogEntry).responseTimeMs : (log as ApiLogEntry).latencyMs}ms
3880
+ </span>
2296
3881
  </div>
2297
3882
  <p className="text-xs text-[var(--text-muted)]">{formatTime(log.createdAt)}</p>
2298
- {log.errorMessage && (
2299
- <p className="text-xs text-red-500 truncate">{log.errorMessage}</p>
3883
+ {log.type === "direct_call" && (log as ApiLogEntry).errorMessage && (
3884
+ <p className="text-xs text-red-500 truncate">{(log as ApiLogEntry).errorMessage}</p>
2300
3885
  )}
2301
3886
  </div>
2302
3887
  ))}
@@ -4056,6 +5641,98 @@ function SettingsSection({ title, icon: Icon, children, defaultOpen = false }: S
4056
5641
  );
4057
5642
  }
4058
5643
 
5644
+ // Team Section Component
5645
+ function TeamSection({ workspace }: { workspace: Workspace | null }) {
5646
+ const [showComingSoon, setShowComingSoon] = useState(false);
5647
+ const [notifyClicked, setNotifyClicked] = useState(false);
5648
+
5649
+ return (
5650
+ <SettingsSection title="Team" icon={Users}>
5651
+ <div className="space-y-4 pt-4">
5652
+ {/* Team Members List */}
5653
+ <div className="space-y-3">
5654
+ {/* Owner - always shown */}
5655
+ <div className="flex items-center justify-between p-4 rounded-xl bg-[var(--surface)]">
5656
+ <div className="flex items-center gap-3">
5657
+ <div className="w-10 h-10 rounded-full bg-[#ef4444]/20 flex items-center justify-center">
5658
+ <Crown className="w-5 h-5 text-[#ef4444]" />
5659
+ </div>
5660
+ <div>
5661
+ <p className="font-medium">{workspace?.email || "Loading..."}</p>
5662
+ <p className="text-sm text-[var(--text-muted)]">Account owner</p>
5663
+ </div>
5664
+ </div>
5665
+ <span className="px-3 py-1 rounded-full bg-[#ef4444]/20 text-[#ef4444] text-xs font-medium">
5666
+ Owner
5667
+ </span>
5668
+ </div>
5669
+ </div>
5670
+
5671
+ {/* Invite Button */}
5672
+ <button
5673
+ onClick={() => setShowComingSoon(true)}
5674
+ className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl border-2 border-dashed border-[var(--border)] text-[var(--text-secondary)] font-medium hover:border-[#ef4444]/50 hover:text-[#ef4444] transition"
5675
+ >
5676
+ <Plus className="w-5 h-5" />
5677
+ Invite Team Member
5678
+ </button>
5679
+
5680
+ {/* Coming Soon Card */}
5681
+ {showComingSoon && (
5682
+ <div className="p-5 rounded-xl bg-gradient-to-br from-[#ef4444]/5 to-[#ef4444]/10 border border-[#ef4444]/20">
5683
+ <div className="flex items-center gap-2 mb-3">
5684
+ <Sparkles className="w-5 h-5 text-[#ef4444]" />
5685
+ <h4 className="font-semibold text-[#ef4444]">Team invites coming soon!</h4>
5686
+ </div>
5687
+ <p className="text-sm text-[var(--text-muted)] mb-4">
5688
+ Share your workspace with team members. They&apos;ll have their own login but share your API access and billing.
5689
+ </p>
5690
+ <div className="flex items-center gap-3">
5691
+ <button
5692
+ onClick={() => {
5693
+ setNotifyClicked(true);
5694
+ setTimeout(() => setNotifyClicked(false), 3000);
5695
+ }}
5696
+ disabled={notifyClicked}
5697
+ className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition ${
5698
+ notifyClicked
5699
+ ? "bg-green-500/20 text-green-500"
5700
+ : "bg-[#ef4444] text-white hover:bg-[#dc2626]"
5701
+ }`}
5702
+ >
5703
+ {notifyClicked ? (
5704
+ <>
5705
+ <Check className="w-4 h-4" />
5706
+ We&apos;ll notify you!
5707
+ </>
5708
+ ) : (
5709
+ <>
5710
+ <Bell className="w-4 h-4" />
5711
+ Get Notified When Ready
5712
+ </>
5713
+ )}
5714
+ </button>
5715
+ <button
5716
+ onClick={() => setShowComingSoon(false)}
5717
+ className="px-4 py-2 rounded-lg text-sm text-[var(--text-muted)] hover:text-[var(--text-primary)] transition"
5718
+ >
5719
+ Dismiss
5720
+ </button>
5721
+ </div>
5722
+ </div>
5723
+ )}
5724
+
5725
+ {/* Info text */}
5726
+ {!showComingSoon && (
5727
+ <p className="text-xs text-[var(--text-muted)] text-center">
5728
+ Team collaboration features are coming soon
5729
+ </p>
5730
+ )}
5731
+ </div>
5732
+ </SettingsSection>
5733
+ );
5734
+ }
5735
+
4059
5736
  function SettingsTab({ workspace, sessionToken }: { workspace: Workspace | null; sessionToken: string | null }) {
4060
5737
  const [isLoadingPortal, setIsLoadingPortal] = useState(false);
4061
5738
  const [portalError, setPortalError] = useState<string | null>(null);
@@ -4197,6 +5874,8 @@ function SettingsTab({ workspace, sessionToken }: { workspace: Workspace | null;
4197
5874
  </div>
4198
5875
  </SettingsSection>
4199
5876
 
5877
+ <TeamSection workspace={workspace} />
5878
+
4200
5879
  <SettingsSection title="Billing" icon={CreditCard}>
4201
5880
  <div className="space-y-4 pt-4">
4202
5881
  {portalError && (