Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 8 additions & 20 deletions frontend/src/components/trustlines/AssetInsights.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Lightbulb } from "lucide-react";
import { InsightsList } from "@/components/dashboard/InsightsList";
import { AssetInsights as AssetInsightsType } from "@/lib/trustline-api";

function fmt(n: number) {
Expand Down Expand Up @@ -54,33 +55,20 @@ interface Props {
}

export function AssetInsights({ data, loading }: Props) {
const insights = data ? buildSentences(data) : [];

return (
<div className="glass-card rounded-2xl p-6 border border-border/50">
<div className="flex items-center gap-2 mb-4">
<Lightbulb className="w-4 h-4 text-accent" />
<h3 className="text-sm font-bold uppercase tracking-wider">Asset Insights</h3>
</div>

{loading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-4 bg-white/5 rounded animate-pulse" style={{ width: `${70 + i * 8}%` }} />
))}
</div>
) : !data || buildSentences(data).length === 0 ? (
<p className="text-xs font-mono text-muted-foreground uppercase tracking-wider">
Insufficient data to generate insights for this asset.
</p>
) : (
<ul className="space-y-2">
{buildSentences(data).map((s, i) => (
<li key={i} className="text-sm text-muted-foreground leading-relaxed flex gap-2">
<span className="text-accent mt-1 shrink-0">›</span>
<span>{s}</span>
</li>
))}
</ul>
)}
<InsightsList
insights={insights}
isLoading={loading}
emptyMessage="Insufficient data to generate insights for this asset."
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { AssetInsights } from "../AssetInsights";
import type { AssetInsights as AssetInsightsType } from "@/lib/trustline-api";

describe("AssetInsights", () => {
const baseData: AssetInsightsType = {
asset_code: "USDC",
asset_issuer: "GABC...XYZ",
largest_holder_pct: 65,
holder_growth_30d: 12,
transfer_volume_30d: 500000,
avg_transfer_volume: 400000,
total_trustlines: 200,
};

it("delegates the loading state to the shared InsightsList", () => {
render(<AssetInsights data={null} loading />);

expect(screen.getByTestId("insights-loading")).toBeInTheDocument();
expect(screen.queryByTestId("insights-list")).not.toBeInTheDocument();
});

it("shows an asset-specific empty message when there is no data", () => {
render(<AssetInsights data={null} loading={false} />);

expect(screen.getByTestId("insights-empty")).toHaveTextContent(
"Insufficient data to generate insights for this asset.",
);
});

it("shows the empty message when data yields no sentences", () => {
const emptyData: AssetInsightsType = {
asset_code: "USDC",
asset_issuer: "GABC...XYZ",
largest_holder_pct: null,
holder_growth_30d: null,
transfer_volume_30d: null,
avg_transfer_volume: null,
total_trustlines: 0,
};

render(<AssetInsights data={emptyData} loading={false} />);

expect(screen.getByTestId("insights-empty")).toBeInTheDocument();
});

it("renders generated insight sentences on the happy path", () => {
render(<AssetInsights data={baseData} loading={false} />);

const list = screen.getByTestId("insights-list");
expect(list).toBeInTheDocument();
expect(screen.getByText(/largest single holder controls 65.0%/i)).toBeInTheDocument();
expect(screen.getByText(/holder count grew by 12/i)).toBeInTheDocument();
});
});