@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.
- package/PRD-ANALYTICS-AGENTS-TEAMS.md +710 -0
- package/PRD-API-CHAINING.md +483 -0
- package/PRD-HARDEN-SHELL.md +18 -12
- package/PRD-LOGS-SUBAGENTS-V2.md +267 -0
- package/convex/_generated/api.d.ts +6 -0
- package/convex/agents.ts +188 -0
- package/convex/chains.ts +1248 -0
- package/convex/logs.ts +94 -0
- package/convex/schema.ts +139 -0
- package/convex/searchLogs.ts +141 -0
- package/convex/teams.ts +243 -0
- package/dist/chain-types.d.ts +187 -0
- package/dist/chain-types.d.ts.map +1 -0
- package/dist/chain-types.js +33 -0
- package/dist/chain-types.js.map +1 -0
- package/dist/chainExecutor.d.ts +122 -0
- package/dist/chainExecutor.d.ts.map +1 -0
- package/dist/chainExecutor.js +454 -0
- package/dist/chainExecutor.js.map +1 -0
- package/dist/chainResolver.d.ts +100 -0
- package/dist/chainResolver.d.ts.map +1 -0
- package/dist/chainResolver.js +519 -0
- package/dist/chainResolver.js.map +1 -0
- package/dist/chainResolver.test.d.ts +5 -0
- package/dist/chainResolver.test.d.ts.map +1 -0
- package/dist/chainResolver.test.js +201 -0
- package/dist/chainResolver.test.js.map +1 -0
- package/dist/execute.d.ts +4 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +3 -0
- package/dist/execute.js.map +1 -1
- package/dist/index.js +478 -3
- package/dist/index.js.map +1 -1
- package/docs/SUBAGENT-NAMING.md +94 -0
- package/landing/public/logos/chattgpt.svg +1 -0
- package/landing/public/logos/claude.svg +1 -0
- package/landing/public/logos/gemini.svg +1 -0
- package/landing/public/logos/grok.svg +1 -0
- package/landing/src/app/page.tsx +12 -21
- package/landing/src/app/workspace/chains/page.tsx +520 -0
- package/landing/src/app/workspace/page.tsx +1903 -224
- package/landing/src/components/AITestimonials.tsx +15 -9
- package/landing/src/components/ChainStepDetail.tsx +310 -0
- package/landing/src/components/ChainTrace.tsx +261 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +14 -2
- package/src/chainExecutor.ts +730 -0
- package/src/chainResolver.test.ts +246 -0
- package/src/chainResolver.ts +658 -0
- package/src/execute.ts +23 -0
- 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
|
-
{/*
|
|
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
|
-
<
|
|
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
|
-
{
|
|
1489
|
-
<div className="flex items-center
|
|
1490
|
-
<
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
<
|
|
1549
|
-
<
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
<
|
|
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
|
-
{
|
|
1576
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
{/*
|
|
1683
|
-
|
|
1684
|
-
<
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
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
|
-
<
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
{/*
|
|
1729
|
-
{
|
|
1730
|
-
<
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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-
|
|
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: "
|
|
1901
|
-
{ provider: "
|
|
1902
|
-
{ provider: "
|
|
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
|
|
1908
|
-
|
|
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
|
-
|
|
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)]">
|
|
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 ? "
|
|
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
|
-
{/*
|
|
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">
|
|
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
|
-
<
|
|
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="
|
|
1990
|
-
|
|
1991
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
|
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, ...(
|
|
3596
|
+
setLogs(prev => [...prev, ...apiLogs].sort((a, b) => b.createdAt - a.createdAt));
|
|
2067
3597
|
} else {
|
|
2068
|
-
setLogs(
|
|
3598
|
+
setLogs(combinedLogs);
|
|
2069
3599
|
}
|
|
2070
|
-
setHasMore(
|
|
2071
|
-
setNextCursor(
|
|
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
|
-
|
|
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
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
3783
|
+
{log.type === "search" ? (
|
|
3784
|
+
<code className="px-2 py-1 rounded bg-blue-500/10 text-blue-500 text-sm font-mono">
|
|
3785
|
+
"{(log as SearchLogEntry).query}"
|
|
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.
|
|
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
|
-
|
|
2264
|
-
{log.
|
|
2265
|
-
|
|
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
|
-
<
|
|
2279
|
-
|
|
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
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
3869
|
+
{log.type === "search" ? (
|
|
3870
|
+
<code className="px-2 py-1 rounded bg-blue-500/10 text-blue-500 font-mono text-xs">
|
|
3871
|
+
"{(log as SearchLogEntry).query}"
|
|
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'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'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 && (
|